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 添加更多功能。
比如:
- 多行注释;
- 有理数运算;
- 宏;
- 延迟求值(
stream
与delay
); - ……
方向 4:实现垃圾回收(GC)
本文档所指导实现的 Mini Lisp 解释器存在内存泄露的问题——一个 Lambda 过程值和它定义时的环境会出现“循环引用”,从而导致双方的 std::shared_ptr
总是持有对方且永远无法被释放。由于 Mini Lisp 本身的内存模型在强引用图下是有环的,故天然地无法通过引用计数的方式来保证内存不会泄露(换句话说不要指望通过 std::weak_ptr
等手段来解决)。为保证不发生内存泄漏,你可以实现一个基于“标记-清除”算法的垃圾回收(Garbage Collection, GC)机制。
具体来说,你需要:
- 将所有的
Value
和EvalEnv
的所有权改为“全局”的。或者说,你可以使用单例模式声明两个集合来保存所有的Value
和EvalEnv
。随后,LambdaValue
不再持有EvalEnv
的所有权,EvalEnv
也不再持有其中符号们所指代的Value
的所有权。(从而,ValuePtr
也应实现为裸指针。) - 当然,在创建求值环境和新的值的时候,你也应该改为向两个全局集合添加元素,然后在相应的位置用裸指针的形式引用它们。
- REPL 模式下,每次求值完成后,进行“标记-清除”算法。具体而言,“标记”阶段你需要从全局求值环境
globalEnv
出发,将所有其符号表持有的Value
标记为“可访问的”。如果遇到了LambdaValue
,还需要将其定义时用到的求值环境(以及它的上级环境)标记为“可访问的”,然后递归地追溯并标记这些求值环境用到的值。“清除”阶段你需要将两个全局集合中未被标记为“可访问的”那些求值环境和值删除并释放掉。 - 类似地,在文件模式下也应当实现如上所述的 GC;但是执行 GC 的时机可能有所变化。