Rust 你自己的小 Lisp
这是我在上一篇文章《通过避免问题来解决问题》中讨论的代码的更完整的演练。
这个项目是将orangeduck的《Build Your Own Lisp》翻译成Rust。这本书非常棒,既是 C 语言的入门书,也是编写解释器的入门书。
这篇文章远不能替代那本书——去读读那本书吧。它很棒。不过,在翻译成 Rust 时,有一些必要的区别值得注意。这篇文章没有包含完整的代码,而是每个概念的示例,对于任何尝试用 Rust 进行类似项目或翻译的人来说可能有用。为了清晰起见,我还删除了大部分调试日志。完整的实现可以在此代码库中找到。
我从这个项目中学到了很多关于 C、解释器和 Rust 的知识,强烈推荐这个练习。无论好坏(可能更糟),我都把这个实现称为blispr
。
拉斯蒂莱恩
首先,我们需要收集一些字符串。我强烈推荐rustyline
一个纯 Rustreadline
实现的 。它提供了行编辑、键盘命令和命令历史记录等功能。您只需执行以下操作:
fn repl(e: &mut Lenv) -> Result<()> {
println!("Blispr v0.0.1");
println!("Use exit(), Ctrl-C, or Ctrl-D to exit prompt");
let mut rl = Editor::<()>::new();
if rl.load_history("./.blispr-history.txt").is_err() {
println!("No history found.");
}
loop {
let input = rl.readline("blispr> ");
match input {
Ok(line) => {
rl.add_history_entry(line.as_ref());
print_eval_result(eval_str(e, &line));
}
Err(ReadlineError::Interrupted) => {
info!("CTRL-C");
break;
}
Err(ReadlineError::Eof) => {
info!("CTRL-D");
break;
}
Err(err) => {
warn!("Error: {:?}", err);
break;
}
}
}
rl.save_history("./.blispr-history.txt")?;
Ok(())
}
fn print_eval_result(v: BlisprResult) {
match v {
Ok(res) => println!("{}", res),
Err(e) => eprintln!("Error: {}", e),
}
}
需要注意的是,我不会将eval_str
可能引发的错误传递给调用者?
——我不希望 blispr 求值错误导致 repl 崩溃。eval_str()
我只想将内部可能发生的任何错误告知用户,eprintln!()
然后再次循环。&mut Lenv
传递的是全局环境——下文将详细介绍。
大部分评估都暗示了工作的核心Ok()
内容:match
eval_str()
pub fn eval_str(e: &mut Lenv, s: &str) -> BlisprResult {
let parsed = BlisprParser::parse(Rule::blispr, s)?.next().unwrap();
let mut lval_ptr = lval_read(parsed)?;
lval_eval(e, &mut *lval_ptr)
}
就是这样,这就是整个解释器。此函数执行评估以文本字符串形式给出的编程语言所需的所有步骤。第一行将解析树存储到parsed
。这会用我们将在下面定义的语义语法标签标记我们的输入字符串。下一行将该树读入位于的 AST lval_ptr
,它将整个程序表示为可以递归评估的 Lisp 值。最后,我们返回使用 完全评估该 AST 的结果lval_eval
,这确保不会发生进一步的评估。在此过程中发生的任何错误都会被运算符捕获?
- 下面我们将看到该Result<T>
别名代表什么。
左室
为了表示 AST,我使用了一个enum
叫做的Rust Lval
:
// The recursive types hold their children in a `Vec`
type LvalChildren = Vec<Box<Lval>>;
// This is a function pointer type
pub type LBuiltin = fn(&mut Lval) -> BlisprResult;
// There are two types of function - builtin and lambda
#[derive(Clone)]
pub enum LvalFun {
Builtin(String, LBuiltin), // (name, function pointer)
Lambda(HashMap<String, Box<Lval>>, Box<Lval>, Box<Lval>), // (environment, formals, body), both should be Qexpr
}
// The main type - all possible Blispr values
#[derive(Debug, Clone, PartialEq)]
pub enum Lval {
Fun(LvalFun),
Num(i64),
Sym(String),
Sexpr(LvalChildren),
Qexpr(LvalChildren),
}
每个变体都带有其内容。当我们读取文本时,每个元素都将被转换为正确的 类型Lval
。例如,像 这样的字符串"4"
将被解析为Lval::Num(4)
。现在,这个值可以在更大的求值上下文中使用。我还fmt::Display
为这种类型实现了 ,它负责定义最终显示给用户的输出字符串。使用自动派生Debug
特征,我们可以得到类似 的结果Lval::Num(4)
,而使用 ,Display
我们只会得到4
:
impl fmt::Display for Lval {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Lval::Blispr(_cells) => write!(f, "<toplevel>"),
Lval::Fun(lf) => match lf {
LvalFun::Builtin(name, _) => write!(f, "<builtin: {}>", name),
LvalFun::Lambda(_, formals, body) => write!(f, "(\\ {} {})", formals, body),
},
Lval::Num(n) => write!(f, "{}", n),
Lval::Sym(s) => write!(f, "{}", s),
Lval::Sexpr(cell) => write!(f, "({})", lval_expr_print(cell)),
Lval::Qexpr(cell) => write!(f, "{{{}}}", lval_expr_print(cell)),
}
}
}
fn lval_expr_print(cell: &[Box<Lval>]) -> String {
let mut ret = String::new();
for i in 0..cell.len() {
ret.push_str(&format!("{}", cell[i]));
if i < cell.len() - 1 {
ret.push_str(" ");
}
}
ret
}
我们有数字、符号、函数(两种不同类型的函数——稍后会详细介绍),以及两种类型的表达式列表——s 表达式和 q 表达式。s 表达式会被当作代码来求值,在第一个位置寻找一个函数;而 q 表达式则被当作数据列表来求值。读入的整个程序将是一个包含 的大文件Lval::Sexpr
,我们只需要对它求值,直到得到一个不需要进一步求值的结果,即Num
、Sym
或Qexpr
。
举个简单的例子,"+ 1 2"
将被存储为Sexpr(Sym("+"), Num(1), Num(2))
。当对其Sexpr
进行求值时,它将首先在环境中查找+
并找到指向内置加法函数的函数指针:Sexpr(Fun(Builtin("+"), Num(1), Num("2")))
。然后,这Sexpr
将被求值作为函数调用,产生Num(3)
,因此无法进一步求值。
这段代码使用了Box
指针类型,它是一个指向堆分配值的智能指针。由于指针Lval
可以存储多种不同类型的数据,因此给定指针的大小Lval
在编译时是未知的。通过仅存储指向堆上值的指针,我们可以构建它们的列表。由于这些Box
指针遵循 Rust 的所有权和借用语义,Rust 会在不再需要它们时自行清理它们。这就是我们在程序的整个生命周期内管理内存的方式——比相应的 C 语言要简洁得多!要创建新的构造函数,我们使用构造函数。例如:
pub fn lval_num(n: i64) -> Box<Lval> {
Box::new(Lval::Num(n))
}
每个变体都有一个对应的方法。调用该方法会在Box::new()
堆上分配相应的空间并返回指针。无需费心处理析构函数——它Box
会尽快销毁自身。
包含类型从空的子项开始Vec
,可以使用lval_add
和进行操作lval_pop
:
// Add lval x to lval::sexpr or lval::qexpr v
pub fn lval_add(v: &mut Lval, x: &Lval) -> Result<()> {
match *v {
Lval::Sexpr(ref mut children)
| Lval::Qexpr(ref mut children)
| Lval::Blispr(ref mut children) => {
children.push(Box::new(x.clone()));
}
_ => return Err(BlisprError::NoChildren),
}
Ok(())
}
// Extract single element of sexpr at index i
pub fn lval_pop(v: &mut Lval, i: usize) -> BlisprResult {
match *v {
Lval::Sexpr(ref mut children)
| Lval::Qexpr(ref mut children)
| Lval::Blispr(ref mut children) => {
let ret = (&children[i]).clone();
children.remove(i);
Ok(ret)
}
_ => Err(BlisprError::NoChildren),
}
}
这两个函数都会改变它们的第一个参数,要么删除,要么添加子项。
错误
与本书的实现不同的是,我没有单独设计一个特定的Lval::Err
AST 变体来处理程序中的错误。相反,我构建了一个单独的错误类型,并Result<T, E>
在整个过程中使用了 风格的错误处理:
#[derive(Debug)]
pub enum BlisprError {
DivideByZero,
EmptyList,
FunctionFormat,
NoChildren,
NotANumber,
NumArguments(usize, usize),
ParseError(String),
ReadlineError(String),
WrongType(String, String),
UnknownFunction(String),
}
为了简化整个使用的类型签名,我有几个类型别名:
pub type Result<T> = std::result::Result<T, BlisprError>;
pub type BlisprResult = Result<Box<Lval>>;
大多数求值函数都会返回一个Result<Box<Lval>, BlisprError>
,现在我只需输入 即可BlisprResult
。少数几个没有成功类型 的函数Box<Lval>
仍然可以使用这个新Result<T>
别名,而不必使用更冗长的内置Result<T, E>
,并且错误类型将始终自动为BlisprError
。
为了能够在整个程序中使用它,我提供了impl From<E> for BlisprError
几种其他类型的错误,std::io::Error
例如pest::error::Error
:
impl<T> From<pest::error::Error<T>> for BlisprError
where
T: Debug + Ord + Copy + Hash,
{
fn from(error: pest::error::Error<T>) -> Self {
BlisprError::ParseError(format!("{}", error))
}
}
impl From<std::io::Error> for BlisprError {
fn from(error: std::io::Error) -> Self {
BlisprError::ParseError(error.to_string())
}
}
这样,我仍然可以对?
返回其他错误类型的函数调用使用运算符BlisprResult
,并且返回的任何错误都会自动转换为适合BlisprError
我的类型。Lval
我们不再存储执行整个计算并最终打印出来的特定错误类型,而是通过类型系统向上冒泡,但你仍然会得到完整的错误pest
:
blispr> eval {* 2 3)
Parse error: --> 1:12
|
1 | eval {* 2 3)
| ^---
|
= expected expr
完全公开:为了写这个pest::error::Error<T>
块,我只是写了我想要的内容,也就是BlisprError::ParseError(format!("{}", error))
安抚编译器。可能有更好的方法,但它确实有效!
解析
本书使用了作者自己的解析器组合器库mpc。如果我要用 C 语言解决另一个类似的问题,我可能会再次使用它。然而,Rust 拥有自己强大的解析生态系统。这个领域的一些重量级人物是nom,combinable和pest。对于这个项目,我选择了 pest ,以尽可能接近源材料。而nom
和combine
会让你定义自己的解析器组合器,pest
你需要提供一个 PEG (或解析表达式语法),与你的代码分开。然后,Pest 使用 Rust 强大的自定义 derive 工具自动为你的语法创建解析。
以下是我使用的这种语言的语法:
COMMENT = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" }
WHITESPACE = _{ (" " | NEWLINE ) }
num = @{ int }
int = { ("+" | "-")? ~ digit+ }
digit = { '0'..'9' }
symbol = @{ (letter | digit | "_" | arithmetic_ops | "\\" | comparison_ops | "&")+ }
letter = { 'a' .. 'z' | 'A' .. 'Z' }
arithmetic_ops = { "+" | "-" | "*" | "/" | "%" | "^" }
comparison_ops = { "=" | "<" | ">" | "!" }
sexpr = { "(" ~ expr* ~ ")" }
qexpr = { "{" ~ expr* ~ "}" }
expr = { num | symbol | sexpr | qexpr }
blispr = { SOI ~ expr* ~ EOI }
它存储在一个单独的文件中,blispr.pest
与源代码一起保存。每一行都细化了一条解析规则。我发现它非常易读,而且易于调整。从底部开始,我们看到一个有效单元由输入开始 (SOI) 和输入结束 (EOI) 之间blispr
的一个或多个 s 组成。An是给定的任何选项。它可以处理注释和空格。我也很喜欢它的语法完全独立于任何 Rust 代码进行维护。使用 Rust 很容易实现这一点:expr
expr
use pest::{iterators::Pair, Parser};
#[cfg(debug_assertions)]
const _GRAMMAR: &str = include_str!("blispr.pest");
#[derive(Parser)]
#[grammar = "blispr.pest"]
pub struct BlisprParser;
现在我们可以使用BlisprParser
结构体将字符串输入解析成解析树parse()
。为了对其进行求值,我们需要构建一个大的Lval
AST:
fn lval_read(parsed: Pair<Rule>) -> BlisprResult {
match parsed.as_rule() {
Rule::blispr => {
let mut ret = lval_blispr();
read_to_lval(&mut ret, parsed)?;
Ok(ret)
}
Rule::expr => lval_read(parsed.into_inner().next().unwrap()),
Rule::sexpr => {
let mut ret = lval_sexpr();
read_to_lval(&mut ret, parsed)?;
Ok(ret)
}
Rule::qexpr => {
let mut ret = lval_qexpr();
read_to_lval(&mut ret, parsed)?;
Ok(ret)
}
Rule::num => Ok(lval_num(parsed.as_str().parse::<i64>()?)),
Rule::symbol => Ok(lval_sym(parsed.as_str())),
_ => unreachable!(), // COMMENT/WHITESPACE etc
}
}
我们将解析树从pest
传递到lval_read
,它将为我们递归构建 AST。此函数查看顶级规则并采取适当的操作,要么分配新的Lval
变体,要么调整 的子项。然后,解析树中的每个子项都作为子项添加到包含 的子项中Lval
,并通过lval_read()
自身将其转换为正确的Lval
。的规则qexpr
类似,其他规则只是Lval
根据给定的类型创建相应的 。有一点很奇怪Rule::expr
——这是一种与任何有效表达式类型匹配的元规则,因此它不是自己的 lval,只是包装了一个更具体的类型。我们只需使用next()
将找到的实际规则传递回lval_read()
。
包含子项的变体使用一个助手,该助手跳过周围的括号,并将实际的子项添加到新的Lval
:
fn read_to_lval(mut v: &mut Lval, parsed: Pair<Rule>) -> Result<()> {
for child in parsed.into_inner() {
if is_bracket_or_eoi(&child) {
continue;
}
lval_add(&mut v, &*lval_read(child)?)?;
}
Ok(())
}
的最终结果lval_read()
将是一个Lval
包含完整解析程序的单精度浮点数,保存在 中lval_ptr
。然后我们调用lval_eval()
,它还会BlisprResult
在将这棵树简化为最易求值的形式后返回 。如果求值成功,我们只需打印结果即可;如果出现任何错误,则打印该错误本身。
环境
在我们深入探讨lval_eval()
它的魔力之前,让我们先暂停一下,聊聊环境。这就是符号如何与函数和值对应的——否则"+"
就只是那个字符,但我们需要将其具体地与加法函数对应起来。
我的想法是否正确,目前尚无定论,但我的处理方式也与书中不同。原文要求创建一个struct
结构体,其中包含两个数组和一个计数器,一个用于存储键,另一个用于存储值。要执行查找,需要找到该键的索引,然后返回值中相同索引处的值。此结构体在程序进入循环之前构建,并手动传递给每个被调用的函数。
相反,我选择了一种HashMap
数据结构,而不是两个具有匹配索引的独立数组:
pub type LEnvLookup = HashMap<String, Box<Lval>>;
#[derive(Debug, PartialEq)]
pub struct Lenv<'a> {
lookup: LEnvLookup,
pub parent: Option<&'a Lenv<'a>>,
}
它Lenv
本身包含查找表和可选的对父级的引用。
我有一些用于获取、设置和枚举内容的辅助方法:
impl Lenv {
// ..
pub fn get(&self, k: &str) -> BlisprResult {
match self.lookup.get(k) {
Some(v) => Ok(v.clone()),
None => {
// if we didn't find it in self, check the parent
// this will recur all the way up to the global scope
match &self.parent {
None => Err(BlisprError::UnknownFunction(k.to_string())),
Some(p_env) => p_env.get(k),
}
}
}
}
// Returns an Lval containing Symbols with each k,v pair in the local env
pub fn list_all(&self) -> BlisprResult {
let mut ret = lval_qexpr();
for (k, v) in &self.lookup {
lval_add(&mut ret, &lval_sym(&format!("{}:{}", k, v)))?;
}
Ok(ret)
}
// add a value to the local env
pub fn put(&mut self, k: String, v: Box<Lval>) {
let current = self.lookup.entry(k).or_insert_with(|| v.clone());
if *v != **current {
// if it already existed, overwrite it with v
*current = v;
}
}
}
从环境中获取值将返回一个全新的值,Lval
其中包含存储内容的副本;打印内容也将返回一个现成的,Lval::Qexpr
其中包含Symbol
与每个条目对应的值。在讨论完求值之后,我们将回到初始化。
环境可选地包含一个父环境,如果在此环境中查找失败,它将尝试父环境。
评估
lval_eval()
调用的函数才是eval_str()
真正进行运算的地方。它将接受一个Lval
(也就是抽象语法树)并递归地求值,得到最终的Lval
。大多数类型的Lval
都已经完全求值了——但任何S-Expression
找到的 都需要被求值,并且Symbol
会在环境中查找。
在查看 Rust 之前,我们先用英语来分解一下:
-
检查 Lval 的类型:
a. Fun | Num | Qexpr - 我们完成了 - 按原样返回 lval。
b. 符号 - 使用 进行环境查找
Lenv::get()
- 例如,查找Sym("+")
,查看 name 处是否存储了函数指针"+"
。返回查找结果,该结果已经是Lval
。c. Sexpr——评估 S 表达式。
-
如果我们完成了这一步,那么我们正在处理一个 S 表达式。其他所有内容都已返回。在继续下一步之前,请先全面评估所有带有 的子元素
lval_eval()
。 -
检查 S 表达式的长度:
a. 0 - 空的 S-Expression - 按原样返回
lval_eval()
b. 1 - 单个表达式 - 弹出该表达式并返回调用它的结果c. 多个表达式(函数调用) - 弹出第一个表达式,并尝试将其用作其余子表达式的函数
在 Rust 中它看起来是这样的:
// Fully evaluate an `Lval`
pub fn lval_eval(e: &mut Lenv, v: &mut Lval) -> BlisprResult {
let child_count;
let mut args_eval;
match v {
Lval::Blispr(forms) => {
// If it's multiple, evaluate each and return the result of the last
args_eval = eval_cells(e, forms)?;
let forms_len = args_eval.len()?;
return Ok(lval_pop(&mut args_eval, forms_len - 1)?);
}
Lval::Sym(s) => {
// If it's a symbol, perform an environment lookup
let result = e.get(&s)?;
// The environment stores Lvals ready to go, we're done
return Ok(result);
}
Lval::Sexpr(ref mut cells) => {
// If it's a Sexpr, we're going to continue past this match
// First recursively evaluate each child with lval_eval()
// grab the length and evaluate the children
child_count = cells.len();
args_eval = eval_cells(e, cells)?;
}
// if it's not a sexpr, we're done, return as is
_ => {
return Ok(Box::new(v.clone()));
}
}
if child_count == 0 {
// It was a Sexpr, but it was empty. We're done, return it
Ok(Box::new(v.clone()))
} else if child_count == 1 {
// Single expression
lval_eval(e, &mut *lval_pop(v, 0)?)
} else {
// Function call
// We'll pop the first element off and attempt to call it on the rest of the elements
let fp = lval_pop(&mut args_eval, 0)?;
lval_call(e, *fp, &mut *args_eval)
}
}
在处理表达式本身之前,对 S 表达式的所有子项进行全面评估的步骤使用了一个辅助程序:
// Given a slice of boxed Lvals, return a single evaluated sexpr
fn eval_cells(e: &mut Lenv, cells: &[Box<Lval>]) -> BlisprResult {
cells.iter().fold(Ok(lval_sexpr()), |acc, c| {
match acc {
Ok(mut lval) => {
lval_add(&mut lval, &*lval_eval(e, &mut c.clone())?)?;
Ok(lval)
}
// it's just a Result so we can bubble errors out of the fold
Err(_) => unreachable!(),
}
})
}
这写为fold
使用一个空的Lval::Sexpr
作为累加器,并将lval_add
每个新结果添加到其中。
函数调用
这几乎使我们完成了所有的事情 - 只剩下最后一步,那就是lval_call()
。
Rust 语言有两种函数:内置函数和用户定义的 lambda 表达式。内置函数由 Rust 实现,是可执行文件本身的一部分。这些函数在创建时存储在环境中:
fn add_builtin(&mut self, name: &str, func: LBuiltin) {
self.put(name.to_string(), lval_builtin(func, name))
}
pub fn new(lookup: Option<LEnvLookup>, parent: Option<&'a Lenv<'a>>) -> Self {
let mut ret = Self {
lookup: lookup.unwrap_or_default(),
parent,
};
// Register builtins
// The "stub" fns are dispatched separately - the function pointer stored is never called
// these are the ones the modify the environment
// Definiton
ret.add_builtin("\\", builtin_lambda);
ret.add_builtin("def", builtin_put_stub);
// etc, lots and lots of builtins
ret.add_builtin("max", builtin_max);
ret
}
每个名称都存储一个指向 Rust 函数的函数指针。这些函数直接操作 lval。例如,这是builtin_head
,它返回 的第一个元素Lval::Qexpr
:
pub fn builtin_head(v: &mut Lval) -> BlisprResult {
let mut qexpr = lval_pop(v, 0)?;
match *qexpr {
Lval::Qexpr(ref mut children) => {
if children.is_empty() {
return Err(BlisprError::EmptyList);
}
debug!("builtin_head: Returning the first element");
Ok(children[0].clone())
}
_ => Err(BlisprError::WrongType(
"qexpr".to_string(),
format!("{:?}", qexpr),
)),
}
}
数学运算都使用相同的函数。它们都接受任意长度的Lval::Num
s 列表,并依次对运行结果和下一个数字进行二元运算,直到列表被消耗完为止:
fn builtin_op(mut v: &mut Lval, func: &str) -> BlisprResult {
let mut child_count;
match *v {
Lval::Sexpr(ref children) => {
child_count = children.len();
}
_ => return Ok(Box::new(v.clone())),
}
let mut x = lval_pop(&mut v, 0)?;
// If no args given and we're doing subtraction, perform unary negation
if (func == "-" || func == "sub") && child_count == 1 {
let x_num = x.as_num()?;
return Ok(lval_num(-x_num));
}
// consume the children until empty
// and operate on x
while child_count > 1 {
let y = lval_pop(&mut v, 0)?;
child_count -= 1;
match func {
"+" | "add" => {
apply_binop!(add, x, y)
}
"-" | "sub" => {
apply_binop!(sub, x, y)
}
"*" | "mul" => {
apply_binop!(mul, x, y)
}
"/" | "div" => {
if y.as_num()? == 0 {
return Err(BlisprError::DivideByZero);
} else {
apply_binop!(div, x, y)
}
}
"%" | "rem" => {
apply_binop!(rem, x, y)
}
"^" | "pow" => {
let y_num = y.as_num()?;
let x_num = x.as_num()?;
let mut coll = 1;
for _ in 0..y_num {
coll *= x_num;
}
x = lval_num(coll);
}
"min" => {
let x_num = x.as_num()?;
let y_num = y.as_num()?;
if x_num < y_num {
x = lval_num(x_num);
} else {
x = lval_num(y_num);
};
}
"max" => {
let x_num = x.as_num()?;
let y_num = y.as_num()?;
if x_num > y_num {
x = lval_num(x_num);
} else {
x = lval_num(y_num);
};
}
_ => unreachable!(),
}
}
Ok(x)
}
这是一个很长的函数 - 但是如果没有我定义的宏,它会更长:
macro_rules! apply_binop {
( $op:ident, $x:ident, $y:ident ) => {
match (*$x, *$y) {
(Lval::Num(x_num), Lval::Num(y_num)) => {
$x = lval_num(x_num.$op(y_num));
continue;
}
_ => return Err(BlisprError::NotANumber),
}
};
}
这使得一些 Lval 类型检查的输入速度更快!它会在Lval::Num
尝试对两个参数进行数值操作之前,确保它们都是正确的,例如apply_binop!(add, x, y)
。这是我第一次尝试定义 Rust 宏,它真的帮了我大忙。
这些调用起来相当容易。因为环境存储了这些 ans 函数指针,所以你可以直接调用函数。我的解决方案有点 hacky,因为一些内置函数需要访问环境,而内置函数则不需要——这些特殊情况会被单独调度,其他所有情况都只需使用以下命令调用即可fp()
:
LvalFun::Builtin(name, fp) => match name.as_str() {
"eval" => builtin_eval(e, args),
"def" => builtin_def(e, args),
"printenv" => builtin_printenv(e),
// Otherwise, just apply the actual stored function pointer
_ => fp(args),
},
调用 aLambda
稍微有点棘手。我们需要构建一个新的环境,添加所有局部绑定,然后要么调用这个新函数,要么返回一个新的、部分应用的 lambda(如果未提供所有局部变量)。这里的机制比较冗长——请参阅此行以了解上下文代码。
这就是我们所有的部分。有了这些,我们lval_eval()
就能处理很多事情了,而且这门语言实际上已经接近可用了。虽然这门语言的实现并不完整,但它是一个学习语言工作原理的绝佳平台!