用 Rust 实现 Lisp 解释器

2023-10-27

  • 文章标题:用 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>(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!("// 									
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

用 Rust 实现 Lisp 解释器 的相关文章

随机推荐

  • Redis(二):基础之五种常见数据结构与使用方法

    五种常见数据结构与使用方法 一 字符串String Redis 中的字符串是一种 动态字符串 这意味着使用者可以修改 它的底层实现有点类似于 Java 中的 ArrayList 有一个字符数组 从源码的 sds h sdshdr 文件 中可
  • 【20170924】C语言每日一练

    程序1 题目 有数字1 2 3 4 能组成多少个互不相同且无重复数字的三位数 都是多少 include
  • docker容器内存分配

    1 和CPU控制一样 docker也提供了若干参数来控制容器的内存使用配额 可以控制容器的swap大小 可用内存大小等各种内存方面的控制 主要有以下参数 memory swappiness 控制进程将物理内存交换到swap分区的倾向 默认系
  • Python selenium(一般不使用,原因:打开浏览器,虽然简单但是性能低)

    selenium使用 1 创建浏览器对象 driver webdriver xxx 2 发送请求 driver get url driver对象常用的属性和方法 1 driver page source 当前标签页浏览器渲染后的页面源代码
  • ESP8266学习笔记(二)

    上篇文章提到了如何使用USB转TTL模块调试ESP8266模块以及该模块的几种工作方式 此篇将会介绍如何实现ESP8266模块与单片机的通信 功能介绍 esp8266模块与stm32单片机的串口三之间互相通信 本人测试的是esp8266模块
  • 【100天精通python】Day37:GUI界面编程_PyQt 从入门到实战(上)_PyQt6基本组件、事件和信号槽、界面设计

    目录 专栏导读 1 PyQt6 简介 1 1 安装 PyQt6 和相关工具 1 2 PyQt6 基础知识 1 2 1 Qt 的基本概念和组件 1 2 2 创建和使用 Qt 窗口 标签 按钮等基本组件 1 2 3 布局管理器 垂直布局 水平布
  • Qt信号槽连接在有默认形参下的情况思考

    写下这个给自己备忘 比如函数 void test int a 0 你在调用端如论是test 3 或者test 都可以正确调用到这个函数 但是 如果放到Qt中的信号槽的话 这个还是值得讲一讲的 不然的话 可能会引起相应的误会 其实说到底 Qt
  • 互联网JAVA面试常问问题(二)

    一 线程有几种创建方式 这是一道比较常见的java线程问题 一般就是两种线程创建方式 继承Thread类 实现Runnable接口 继承Thread类 public class MyThread extends Thread private
  • Android资源文件中颜色使用的总结

    本文对Android颜色的使用做总结 重点介绍颜色在资源文件中的创建和颜色的选择器的创建和使用 一 在xml中使用颜色资源文件和颜色选择器文件 一 颜色资源文件的创建 1 创建资源文件 如图所示 2 编辑colors xml资源文件 如图所
  • halcon 与PLC串口通信解决方案

    OpSystem environment OS if OpSystem Windows NT open serial COM1 SerialHandle else open serial dev tty SerialHandle endif
  • vite项目中导入图片后报找不到模块处理方法

    vite项目 typescript的项目中 导入图片后报找不到模块处理方法 问题 在使用ts书写代码时 导入本地文件夹中图片 会出现报错 找不到模块 但是又能正常使用该图片 这样的报错启动项目是没有问题 但是最后打包会报错 所以不得不处理
  • MPP架构、常见OLAP引擎分析

    MPP架构 常见OLAP引擎分析 一 MPP架构 1 SMP 2 NUMA 3 MPP 二 批处理架构和MPP架构 三 MPP架构的OLAP引擎 1 只负责计算 不负责存储的引擎 1 Impala 2 Presto 2 既负责计算 又负责存
  • 性能测试报告全解析:如何编写一份专业的性能测试报告!

    一 背景 性能测试是软件开发过程中非常重要的一环 它可以帮助开发人员和质量保障人员评估软件在不同负载下的表现 找出瓶颈并优化性能 从而提高用户的满意度 而一份专业的性能测试报告 则是评估软件性能的重要成果之一 因此今天我们将分享一份完整的性
  • 解决ThinkPHP3.2 将Debug 关闭 设置为False 报页面错误 请稍后再试

    1 最近系统要上线 就把Index php中的debug 关闭 设置成false 结果出现如下的错误 2 修改config php文件 加入 SHOW ERROR MSG gt TRUE 后 显示错误信息 又报如下的错 这才是真正的错误信息
  • Windows Server 2022 下 Hyper-V NAT外网访问配置

    Windows Server 2022 下 Hyper V NAT外网访问配置 一 前言 二 安装 配置虚拟网卡 三 角色安装 四 路由和远程访问服务配置 五 DHCP服务器配置 六 DNS服务器配置 七 Hyper V配置 八 结果 本篇
  • Docker安装Nginx并修改Nginx配置文件

    一 Docker安装Nginx 1 首先在虚拟机上要确保你已经启动了docker 2 其次登录DockerHub官网 然后搜索nginx 然后在虚拟机里面输入docker pull nginx 就可以下载nginx的镜像了 3 注意下载完以
  • Python爬虫——SQLite数据库

    SQLite数据库 声明 本篇文章仅仅是个人的浅薄理解 只是在爬虫过程中使用 其中若有不当之处 烦请理解并指出 谢谢 使用语言 Python 开发环境 pyCharm 在Python中使用SQLite数据库 主要使用按四个步骤进行 链接数据
  • PHP 微信公众号拉取微信授权

    PHP 微信公众号拉取微信授权 TP6B版本 前期准备工作 1 拿到开发者id 开发秘钥 设置IP白名单 2 设置业务域名 接口安全域名 授权域名 3 根据前端传入code参数获得用户微信信息 代码如下 获得用户信息 public func
  • vue diff算法

    vue中的diff算法概念 虽然点进来的大家应该都知道diff算法是什么 不过按照流程还是要简单说一下 按我个人的理解 Vue的diff算法是对新旧两条虚拟DOM进行逐层比较并更新真实DOM diff算法是平级比较 不考虑跨级的情况 采用深
  • 用 Rust 实现 Lisp 解释器

    文章标题 用 Rust 实现 Lisp 解释器 深度参考 https stopachka essay dev post 5 risp in rust lisp 本文作者 suhanyujie 文章来自 https github com su
Powered by Hwhale