用 Rust 实现 Lisp 解释器

  • 文章标题:用 Rust 实现 Lisp 解释器
  • 深度参考:https://stopachka.essay.dev/post/5/risp-in-rust-lisp
  • 本文作者:suhanyujie
  • 文章来自:https://github.com/suhanyujie/rust-cookbook-note
  • 博客链接:https://ishenghuo.cnblogs.com
  • ps:水平有限,翻译不当之处,还请指正,谢谢!

前言

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

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

Lisp

在开始前,我们需要了解一些关于 lisp 的背景知识。Lisp 是一种 schema(一种高级编程语言)方言的实现。查阅了下百度百科,其描述可读性不强,还不如这个 Lisp 教程中所描述的:

约翰·麦卡锡发明LISP于1958年,FORTRAN语言的发展后不久。首次由史蒂夫·拉塞尔实施在IBM704计算机上。
它特别适合用于人工智能方案,因为它有效地处理的符号信息。
Common Lisp的起源,20世纪80年代和90年代,分别接班人Maclisp像ZetaLisp和NIL(Lisp语言的新实施)等开发。
它作为一种通用语言,它可以很容易地扩展为具体实施。
编写Common Lisp程序不依赖于机器的具体特点,如字长等。

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

sudo apt-get install sbcl

然后,在命令行中输入 sbcl,即可进入 repl:

$ 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 2)//=> 17
(- 10 5 2) //=> 3

输入简单的 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&
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值