用 Rust 实现 Lisp 解释器

前言

一段时间没有写 Rust 了,感觉有些生疏了,打算找个 Rust 小项目复习一下。在芽之家博客看到了这个博文,讲的是用 Rust 实现 lisp。有感兴趣的同学,可以一起看看。

作者介绍到,这是他的第一个练手项目,有些地方可能会实现的不是很好,但我觉得也是很有参考价值的,尤其是对于我这样的 Rust 新手。此外,作者还提到了另一篇 python 实现 lisp,这应该也是参考资料之一。

Lisp

在开始前,我们需要了解一些关于 lisp 的背景知识。Lisp 是一种高阶编程语言,在其基础上演变出了很多种方言,如:Scheme、Common Lisp 等。查阅了下百度百科,其描述可读性不强,建议阅读维基百科的描述,或者这个 Lisp 教程

在实现一个 Lisp(子集)的解析器之前,先要了解 Lisp 的语法规则。如果你想大概了解一下它的语法和简单使用,可以自己在本地安装一个环境,并尝试。这里以 Ubuntu 20.04 为例。可通过以下命令安装一个 common lisp 的实现 —— sbcl,用于熟悉 lisp:

sudo apt-get install sbcl

然后,在命令行中输入 sbcl,即可进入它的交互式命令行:

$ sbcl
This is SBCL 2.0.1.debian, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.

输入一个加法运算试一试:

$ * (+ 1 2)
3

可以看到,能得到计算后地结果 —— 3。

关于更多关于 Lisp 的语法在这里就不详细说明了,可以参考这个教程进行进一步学习。

Lisp 的算术运算

为了能尽快地实现目标,我们只是简单地实现一个类似于计算器的运算功能,别看只是一个小小的计算器,但也包含了很多的基础知识。

在开始之前,我们先确定好最终的目标,我们最终实现的效果如下:

(+ 10 5) //=> 15
(- 10 5) //=> 5

输入简单的 lisp 程序,就能输出对应的计算结果。在开始之前,先介绍一下我们的程序执行,所经历的大体过程:

程序 -> parse(解析) -> 抽象语法树 -> eval(执行) -> 结果

这个过程中的 parse 和 eval 就是我们要实现的功能。比如下面这个程序示例:

$ (+ 1 2)
3
$ (* 2 3)
6

换句话说,就是我们需要将我们输入的源代码解析转换成语法树,然后执行语法树就能得到我们想要的结果。而源码中,我们只需有三类输入:

  • 符号
  • 数值
  • 列表

将其用 Rust 枚举类型表示,如下:

#[derive(Clone)]
enum RispExp {
  Symbol(String),
  Number(f64),
  List(Vec<RispExp>),
}

你可能有些疑惑,没关系,我们继续向后看。

在解析源码时,我们会遇到错误,因此需要定义错误类型:

enum RispErr {
    Reason(String),
}

如果你想定义更健壮、好用的错误类型,可以参考这个。但这里,为了简化实现,我们只是将错误类型定义成一个枚举变体 Reason(String),一旦遇到异常,我们将异常信息装入其中,返回给调用方即可。

我们还需要一个作用域类型,用它来存储定义的变量、内置函数等。

#[derive(Clone)]
struct RispEnv {
  data: HashMap<String, RispExp>,
}

解析

根据前面的过程描述,我们要将源码解析成语法树,也就是 RispExp 的表示形式。这样做之前,我们需要将源码解析成一个一个 token。

比如我们的输入是 (+ 10 5),将其 token 化的结果是 ["(", "+", "10", "5", ")"]。使用 Rust 实现如下:

fn tokenize(expr: String) -> Vec<String> {
    expr.replace("(", " ( ")
        .replace(")", " ) ")
        .split_whitespace()
        .map(|x| x.to_string())
        .collect()
}

根据 lisp 表达式的规则,表达式一般都是由小括号包裹起来的,为了更好的通过空格分割 token,我们将小括号替换为两边各带有一个空格的括号。然后通过 split_whitespace 函数将字符串进行分割,并把每段字符串转换成带所有权的字符串,最后通过 collect 收集,以字符串数组的形式存放到变量中。

然后通过 parse 函数将其转化成 RispExp 类型结构:

fn parse<\'a>(tokens: &\'a [String]) -> Result<(RispExp, &\'a [String]), RispErr> {
    let (token, rest) = tokens
        .split_first()
        .ok_or(RispErr::Reason("could not get token".to_string()))?;
    match &token[..] {
        "(" => read_seq(rest),
        ")" => Err(RispErr::Reason("unexpected `)`".to_string())),
        _ => Ok((parse_atom(token), rest)),
    }
}

fn read_seq<\'a>(tokens: &\'a [String]) -> Result<(RispExp, &\'a [String]), RispErr> {
    let mut res: Vec<RispExp> = vec![];
    let mut xs = tokens;
    loop {
        let (next_token, rest) = xs
            .split_first()
            .ok_or(RispErr::Reason("could not find closing `)`".to_string()))?;
        if next_token == ")" {
            return Ok((RispExp::List(res), rest));
        }
        let (exp, new_xs) = parse(&xs)?;
        res.push(exp);
        xs = new_xs;
    }
}

得到 token 列表后,我们对 token 逐个解析,通过 split_first 取出 token 列表中的第一个 token,以及第一个以外的其余元素。

对第一个 token 进行模式匹配:

  • 如果表达式以 ( 开头,则调用 read_seq 读取表达式剩余部分的 token
  • 如果表达式以 ) 开头,则意味着当前表达式是错误的表达式。
  • 以上之外,则是要按正常情况解析 lisp 表达式中的原子 —— atom。parse_atom 的实现如下:
fn parse_atom(token: &str) -> RispExp {
    let potential_float: Result<f64, ParseFloatError> = token.parse();
    match potential_float {
        Ok(v) => RispExp::Number(v),
        Err(_) => RispExp::Symbol(token.to_string().clone()),
    }
}

根据语法规则,一个原子是一个数字连续字符或字符串,它包括数字和特殊字符。

我们先尝试将其解析为数值类型,如果解析失败,则意味着它是字符串 —— RispExp::Symbol(token.to_string().clone())。

我们会在全局符号表中存储变量的定义和函数定义,因此我们需要扩展一下 RispExp:

#[derive(Clone)]
enum RispExp {
    Symbol(String),
    Number(f64),
    List(Vec<RispExp>),
    Func(fn(&[RispExp]) -> Result<RispExp, RispErr>), // new
}

我们先创建一个存储特定符号的容器,每一个符号都有特殊的功能:

fn default_env() -> RispEnv {
    let mut data: HashMap<String, RispExp> = HashMap::new();
    data.insert(
        "+".to_string(),
        RispExp::Func(|args: &[RispExp]| -> Result<RispExp, RispErr> {
            let sum = parse_list_of_floats(args)?
                .iter()
                .fold(0.0, |sum, a| sum + a);
            Ok(RispExp::Number(sum))
        }),
    );
    data.insert(
        "-".to_string(),
        RispExp::Func(|args: &[RispExp]| -> Result<RispExp, RispErr> {
            let floats = parse_list_of_floats(args)?;
            let first = *floats
                .first()
                .ok_or(RispErr::Reason("expected at least one number".to_string()))?;
            let sum_of_rest = floats[1..].iter().fold(0.0, |sum, a| sum + a);

            Ok(RispExp::Number(first - sum_of_rest))
        }),
    );

    RispEnv { data }
}

这里我们先实现 +- 运算符的功能。并且为了简化实现,我们先简单粗暴地认为参数都是合法的数值类型,可以通过 parse_list_of_floats 解析这些参数:

fn parse_list_of_floats(args: &[RispExp]) -> Result<Vec<f64>, RispErr> {
    args.iter().map(|x| parse_single_float(x)).collect()
}

fn parse_single_float(exp: &RispExp) -> Result<f64, RispErr> {
    match exp {
        RispExp::Number(num) => Ok(*num),
        _ => Err(RispErr::Reason("expect a number".to_string())),
    }
}

执行

接下来是实现 eval(程序执行)部分了。

  • 1.程序体(表达式)的第一部分如果是标识符,则在全局环境中查询该标识符,如果存在,则返回(如果是 +- 等操作符,则返回 RispExp::Func 类型的操作逻辑实现)。
  • 2.如果是数值,则返回该数值
  • 3.如果是列表,则尝试步骤一。即先返回 RispExp::Func(函数类型),然后列表中的其他原子作为参数执行该函数。
fn eval(exp: &RispExp, env: &mut RispEnv) -> Result<RispExp, RispErr> {
    match exp {
        RispExp::Symbol(k) => env
            .data
            .get(k)
            .ok_or(RispErr::Reason(format!("unexpected symbol k={}", k)))
            .map(|x| x.clone()),
        RispExp::Number(_a) => Ok(exp.clone()),
        RispExp::List(list) => {
            let first_form = list
                .first()
                .ok_or(RispErr::Reason("expected a non-empty list".to_string()))?;
            let arg_forms = &list[1..];
            let first_eval = eval(first_form, env)?;
            match first_eval {
                RispExp::Func(f) => {
                    let args_eval = arg_forms
                        .iter()
                        .map(|x| eval(x, env))
                        .collect::<Result<Vec<RispExp>, RispErr>>();
                    f(&args_eval?)
                }
                _ => Err(RispErr::Reason("first form must be a function".to_string())),
            }
        }
        RispExp::Func(_) => Err(RispErr::Reason("unexpected form".to_string())),
    }
}

前面提到过,我们要实现一个简单的计算器,而 lisp 的计算表达式一般是以符号原子开始的,如:(+ 1 2)

当把这个表达式转换为 RispExp 结构后的形式类似于:

// 伪代码
PlusFunc(
  num1,
  num2,
  ...
)

我们先通过 + 匹配到事先在 default_env 中注册好的函数 f,然后向该函数中传入第一个原子之后的所有参数:f(num1, num2),就能得到执行结果。

REPL

REPL 的全称是 Read Evel Print Loop,表示一种交互形式:读取 -> 执行 -> 打印结果 -> 循环。

针对前面实现的 lisp 子集,我们可以为其实现一个 repl,用于更好的使用该“lisp 解释器”。

我们要做的很简单,读取用户输入,然后解析执行,把执行结果打印出来,然后不断地循环整个过程。那接下来,把解释器的实现用循环包裹起来试试:

fn parse_eval(expr: String, env: &mut RispEnv) -> Result<RispExp, RispErr> {
    let (parsed_exp, _) = parse(&tokenize(expr))?;
    let evaled_exp = eval(&parsed_exp, env)?;
    Ok(evaled_exp)
}

获取用户输入的表达式,再调用 parse_eval:

fn slurp_expr() -> String {
    let mut expr = String::new();
    io::stdin()
        .read_line(&mut expr)
        .expect("Failed to read line");
    expr
}

pub fn run_repl() {
    let env = &mut default_env();
    loop {
        println!("risp >");
        let expr = slurp_expr();
        match parse_eval(expr, env) {
            Ok(res) => println!("//