Rust 你自己的小 Lisp

2025-06-04

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),
    }
}

Enter fullscreen mode Exit fullscreen mode

需要注意的是,我不会将eval_str可能引发的错误传递给调用者?——我不希望 blispr 求值错误导致 repl 崩溃。eval_str()我只想将内部可能发生的任何错误告知用户,eprintln!()然后再次循环。&mut Lenv传递的是全局环境——下文将详细介绍。

大部分评估都暗示了工作的核心Ok()内容matcheval_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)
}
Enter fullscreen mode Exit fullscreen mode

就是这样,这就是整个解释器。此函数执行评估以文本字符串形式给出的编程语言所需的所有步骤。第一行将解析树存储到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),
}
Enter fullscreen mode Exit fullscreen mode

每个变体都带有其内容。当我们读取文本时,每个元素都将被转换为正确的 类型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
}
Enter fullscreen mode Exit fullscreen mode

我们有数字、符号、函数(两种不同类型的函数——稍后会详细介绍),以及两种类型的表达式列表——s 表达式和 q 表达式。s 表达式会被当作代码来求值,在第一个位置寻找一个函数;而 q 表达式则被当作数据列表来求值。读入的整个程序将是一个包含 的大文件Lval::Sexpr,我们只需要对它求值,直到得到一个不需要进一步求值的结果,即NumSymQexpr

举个简单的例子,"+ 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))
}
Enter fullscreen mode Exit fullscreen mode

每个变体都有一个对应的方法。调用该方法会在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),
    }
}
Enter fullscreen mode Exit fullscreen mode

这两个函数都会改变它们的第一个参数,要么删除,要么添加子项。

错误

与本书的实现不同的是,我没有单独设计一个特定的Lval::ErrAST 变体来处理程序中的错误。相反,我构建了一个单独的错误类型,并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),
}
Enter fullscreen mode Exit fullscreen mode

为了简化整个使用的类型签名,我有几个类型别名:

pub type Result<T> = std::result::Result<T, BlisprError>;
pub type BlisprResult = Result<Box<Lval>>;
Enter fullscreen mode Exit fullscreen mode

大多数求值函数都会返回一个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())
    }
}
Enter fullscreen mode Exit fullscreen mode

这样,我仍然可以对?返回其他错误类型的函数调用使用运算符BlisprResult,并且返回的任何错误都会自动转换为适合BlisprError我的类型。Lval我们不再存储执行整个计算并最终打印出来的特定错误类型,而是通过类型系统向上冒泡,但你仍然会得到完整的错误pest

blispr> eval {* 2 3)
Parse error:  --> 1:12
  |
1 | eval {* 2 3)
  |            ^---
  |
  = expected expr
Enter fullscreen mode Exit fullscreen mode

完全公开:为了写这个pest::error::Error<T>块,我只是写了我想要的内容,也就是BlisprError::ParseError(format!("{}", error))安抚编译器。可能有更好的方法,但它确实有效!

解析

本书使用了作者自己的解析器组合器库mpc。如果我要用 C 语言解决另一个类似的问题,我可能会再次使用它。然而,Rust 拥有自己强大的解析生态系统。这个领域的一些重量级人物是nomcombinablepest。对于这个项目,我选择了 pest ,以尽可能接近源材料。而nomcombine会让你定义自己的解析器组合器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 }
Enter fullscreen mode Exit fullscreen mode

它存储在一个单独的文件中,blispr.pest与源代码一起保存。每一行都细化了一条解析规则。我发现它非常易读,而且易于调整。从底部开始,我们看到一个有效单元由输入开始 (SOI) 和输入结束 (EOI) 之间blispr的一个或多个 s 组成。An是给定的任何选项。它可以处理注释和空格。我也很喜欢它的语法完全独立于任何 Rust 代码进行维护。使用 Rust 很容易实现这一点:exprexpr

use pest::{iterators::Pair, Parser};

#[cfg(debug_assertions)]
const _GRAMMAR: &str = include_str!("blispr.pest");

#[derive(Parser)]
#[grammar = "blispr.pest"]
pub struct BlisprParser;
Enter fullscreen mode Exit fullscreen mode

现在我们可以使用BlisprParser结构体将字符串输入解析成解析树parse()。为了对其进行求值,我们需要构建一个大的LvalAST:

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
    }
}
Enter fullscreen mode Exit fullscreen mode

我们将解析树从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(())
}
Enter fullscreen mode Exit fullscreen mode

的最终结果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>>,
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

从环境中获取值将返回一个全新的值,Lval其中包含存储内容的副本;打印内容也将返回一个现成的,Lval::Qexpr其中包含Symbol与每个条目对应的值。在讨论完求值之后,我们将回到初始化。

环境可选地包含一个父环境,如果在此环境中查找失败,它将尝试父环境。

评估

lval_eval()调用的函数才是eval_str()真正进行运算的地方。它将接受一个Lval(也就是抽象语法树)并递归地求值,得到最终的Lval。大多数类型的Lval都已经完全求值了——但任何S-Expression找到的 都需要被求值,并且Symbol会在环境中查找。

在查看 Rust 之前,我们先用英语来分解一下:

  1. 检查 Lval 的类型:

    a. F​​un | Num | Qexpr - 我们完成了 - 按原样返回 lval。

    b. 符号 - 使用 进行环境查找Lenv::get()- 例如,查找Sym("+"),查看 name 处是否存储了函数指针"+"。返回查找结果,该结果已经是Lval

    c. Sexpr——评估 S 表达式。

  2. 如果我们完成了这一步,那么我们正在处理一个 S 表达式。其他所有内容都已返回。在继续下一步之前,请先全面评估所有带有 的子元素lval_eval()

  3. 检查 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

在处理表达式本身之前,对 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!(),
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

这写为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
}
Enter fullscreen mode Exit fullscreen mode

每个名称都存储一个指向 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),
        )),
    }
}
Enter fullscreen mode Exit fullscreen mode

数学运算都使用相同的函数。它们都接受任意长度的Lval::Nums 列表,并依次对运行结果和下一个数字进行二元运算,直到列表被消耗完为止:

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)
}
Enter fullscreen mode Exit fullscreen mode

这是一个很长的函数 - 但是如果没有我定义的宏,它会更长:

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),
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

这使得一些 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),
},
Enter fullscreen mode Exit fullscreen mode

调用 aLambda稍微有点棘手。我们需要构建一个新的环境,添加所有局部绑定,然后要么调用这个新函数,要么返回一个新的、部分应用的 lambda(如果未提供所有局部变量)。这里的机制比较冗长——请参阅此行以了解上下文代码。

这就是我们所有的部分。有了这些,我们lval_eval()就能处理很多事情了,而且这门语言实际上已经接近可用了。虽然这门语言的实现并不完整,但它是一个学习语言工作原理的绝佳平台!

文章来源:https://dev.to/decidously/rust-your-own-lisp-50an
PREV
Rust 与 C++ 的比较 C++ 能做什么而 Rust 不能?
NEXT
使用 bs-socket 在 ReasonML 中实现实时通信