Lv.7+ 新的开始

在前面记账中,你已经完成了一个 Lisp 的解释器——虽然这个 Lisp 语言相比真正的 Lisp 方言简单很多,但是你在这个过程中也了解了许多关于软件工程、编译原理、函数式编程的各种各样的思想。愿你在辛苦的汗水之下,收获了一份幸福的成就感。

接下来,如果你还想继续完善你的解释器,我在这里提供若干思路,仅供抛砖引玉。我们助教非常期待同学们在这里的创新!

方向 1:添加更多的内置过程

最简单的,就是在内置过程上做更多工作。比如,你可以提供更多的数学函数,从而让程序执行更复杂的运算。

我们的 Mini Lisp 没有提供任何字符串操作函数。你可以添加一些如字符串与整数的转换、字符串的拼接、访问、操作等等内置过程。

此外,你可以结合一些其它的第三方库,实现非常复杂的功能。比如,你可以综合一些 2D 绘图库,使得你的解释器支持通过一些内置过程,在窗口中绘制各种各样图案(然后你就能用 Lisp 写一个《黑白棋》了,哈哈)。

你还可以尝试实现 read 内置过程——从标准输入读取一个表达式(外部表示)。但这个功能实现起来非常复杂;你可以先尝试实现 readline 类似的内置过程,读取一行输入,然后再 eval。这个会比 read 容易得多。

方向 2:改善用户体验

我们的 REPL 设计得非常简单。你可以在此基础上做更多改善用户体验的功能;比如支持连续多行的输入:

>>> (define (f x)      ; 此处换行,解释器继续等待输入
...         (* x x))
()
>>> (f 2)
4

更进一步,你可以实现更复杂的功能如光标自动定位、代码高亮等等(难度不小哦!)。

方向 3:设计更复杂的语法

我们的 Mini Lisp 语言灵感来自 R5RS,也就是 Scheme。你可以参考 Scheme 语言的更多语法,来给 Mini Lisp 添加更多功能。

比如:

  • 多行注释;
  • 有理数运算;
  • 宏;
  • 延迟求值(streamdelay);
  • ……

方向 4:实现垃圾回收(GC)

本文档所指导实现的 Mini Lisp 解释器存在内存泄露的问题——一个 Lambda 过程值和它定义时的环境会出现“循环引用”,从而导致双方的 std::shared_ptr 总是持有对方且永远无法被释放。由于 Mini Lisp 本身的内存模型在强引用图下是有环的,故天然地无法通过引用计数的方式来保证内存不会泄露(换句话说不要指望通过 std::weak_ptr 等手段来解决)。为保证不发生内存泄漏,你可以实现一个基于“标记-清除”算法的垃圾回收(Garbage Collection, GC)机制。

具体来说,你需要:

  • 将所有的 ValueEvalEnv 的所有权改为“全局”的。或者说,你可以使用单例模式声明两个集合来保存所有的 ValueEvalEnv。随后,LambdaValue 不再持有 EvalEnv 的所有权,EvalEnv 也不再持有其中符号们所指代的 Value 的所有权。(从而,ValuePtr 也应实现为裸指针。)
  • 当然,在创建求值环境和新的值的时候,你也应该改为向两个全局集合添加元素,然后在相应的位置用裸指针的形式引用它们。
  • REPL 模式下,每次求值完成后,进行“标记-清除”算法。具体而言,“标记”阶段你需要从全局求值环境 globalEnv 出发,将所有其符号表持有的 Value 标记为“可访问的”。如果遇到了 LambdaValue,还需要将其定义时用到的求值环境(以及它的上级环境)标记为“可访问的”,然后递归地追溯并标记这些求值环境用到的值。“清除”阶段你需要将两个全局集合中未被标记为“可访问的”那些求值环境和值删除并释放掉。
  • 类似地,在文件模式下也应当实现如上所述的 GC;但是执行 GC 的时机可能有所变化。
Last Updated:
Contributors: Guyutongxue