目录
前文列表
《用 C 语言开发一门编程语言 — 交互式解析器》
《用 C 语言开发一门编程语言 — 跨平台的可移植性》
《用 C 语言开发一门编程语言 — 语法解析器》
《用 C 语言开发一门编程语言 — 抽象语法树》
《用 C 语言开发一门编程语言 — 异常处理》
《用 C 语言开发一门编程语言 — S-表达式》
《用 C 语言开发一门编程语言 — Q-表达式》
《用 C 语言开发一门编程语言 — 变量元素设计》
《用 C 语言开发一门编程语言 — 基于 Lambda 表达式的函数设计》
《用 C 语言开发一门编程语言 — 条件分支》
《用 C 语言开发一门编程语言 — 字符串与文件加载》
原生类型
目前我们的语言只封装了 C 语言中的原生 long 和 char* 类型。对于你想要做任何更加有用的计算的情况来说,这是非常有限的。更何况,我们对这些数据类型的操作也非常有限。理想情况下,我们的语言应该封装所有原生 C 数据类型,并允许操作它们的方法。其中一个最重要的补充是允许操作十进制数。为此,您应当封装 double 类型和相关运算。而且,随着数据类型的增多,我们需要确保运算符例如 + 和 - 能够很好地在它们各自或者集合上起效。
对于希望使用其语言中的十进制和浮点数进行计算的人来说,添加对原生类型的支持应该是相对来说比较有趣的。
用户定义的类型
除了添加对原生类型的支持之外,最好让用户能够添加自己的新类型,就像我们在 C 中使用结构体一样。用于执行此操作的语法或方法将取决于程序员。这是一个非常重要的部分,能使我们的语言可用于任何大小合理的项目当中去。
这个任务对于那些对如何开发语言有特定想法的人,以及对语言本身,希望其最终设计看起来像什么的人来说,这可能会很有趣。
[] 方括号的补充
有些语言使用方括号 [] 表示列表。这种语法糖用于例如 list 100 (+ 10 20) 300
的情况。通过 [],你可以写成 [100 (+ 10 20) 300]
。对于希望尝试添加额外语法的人来说,这应该是一个简单的补充。
操作系统交互
这意味着封装所有 C 的功能,例如fread,fwrite,fgetc等在 Lispy 中的等同物。这是一项相当直观的任务,但确实需要编写大量的封装函数。这就是为什么到目前为止我们还没有为我们的语言做过类似的工作。
在类似的说明中,让我们的语言能够有权限并且适当地进行系统的调用是很好的。我们应该让它能够更改目录,列出目录中的文件以及诸如此类的功能。这是一项简单的任务,但同样需要封装大量的 C 函数。这对于任何想要将语言当作真实情况下的脚本语言来说,是极为重要的。
希望利用他们的语言进行简单的脚本编写任务和字符串操作的人可能对实现此项目感兴趣。
宏
许多其他 Lisps 允许编写类似于 (def x 100)
定义值 100 到 x 上。在我们的 Lispy 中,这不会起作用,因为它会尝试计算 x 在环境中的存储为 x 的任何值。在其他 Lisps 中,这些函数称为宏,当遇到它们时,它们会停止对其参数的计算,并对它们进行未计算的操作。它们让你编写看起来像普通函数调用的东西,但实际上做的是复杂而有趣的事情。
语言中如果有这些将会很有趣。它们使语言可以为某些工作赋予一些魔力。在许多情况下,这可以使语法更好或允许用户不需要太过于单调。
我喜欢我们的语言在没有宏的时候,处理 def 和 if 的过程。但是如果你不喜欢它,也就是语言当前的工作方式,并希望它与传统的 Lisp 更相似,那么这可能是你有兴趣实现的东西。
变量哈希表
当我们查找变量名的时候,我们只是对当前环境中的所有变量进行线性搜索。我们定义的变量越多,这就变得越来越低效。
更有效的方法是实现哈希表。此技术将变量名称转换为整数,并使用此函数将索引转换为已知大小的数组,以查找与此符号关联的值。这是编程中非常重要的数据结构,并且由于其在重负载下的出色性能而无处不在。
任何有兴趣了解更多有关数据结构和算法的人都会很聪明地尝试实现这种数据结构或其中一种变体。
池分配
我们的 Lispy 很简单,但速度不快。它的性能与 Python 和 Ruby 相似。我们程序中的大多数性能开销来自这样一个问题:几乎任何过程都需要我们构造和析构 lval。因此,我们必须经常调用 malloc,这是一个很慢的函数,因为它需要操作系统为我们做一些管理。在进行计算时,会有很多 lval 类型的复制,分配和释放。
如果我们希望减少这种开销,我们需要降低 malloc 的调用次数。执行此操作的一种方法是让在程序开始时就调用一次 malloc,分配大量内存。然后我们应该调用一些函数来替换我们所有的 malloc 调用,这些函数分割并分配这个内存以便在程序中使用。这意味着我们正在模拟操作系统的一些行为,但是只是以更快的本地方式进行。这种想法称为内存池分配,是游戏开发和其他对性能特别重视的应用程序中常用的技术。
垃圾回收
几乎所有其他 Lisps 实现都为我们的变量分配不同的变量。它们不会在环境中存储值的副本,而是实际上直接指向它的指针或引用。因为使用指针而不是副本,就像在 C 中一样,使用大型数据结构时所需的开销要少得多。
如果我们存储指向值而不是副本的指针,我们需要确保在某些其他值尝试使用之前,指向的数据不会被删除。我们希望在不再引用它时删除它。执行此操作的一种方法称为 Mark 和 Sweep,用于监视环境中的值以及已分配的每个值。当一个变量被放入环境中时,它和它引用的所有内容都会被标记出来。然后,当我们希望释放内存时,我们可以迭代每个分配,并删除任何未标记的内容。
这称为垃圾回收,是许多编程语言不可或缺的一部分。与池分配一样,实现垃圾回收器不需要很复杂,但确实需要仔细完成,实现这一点对于使这种语言适用于处理大量数据至关重要。
尾调用优化
我们的编程语言使用递归来进行循环。虽然这在概念上是一种非常聪明的方法,但实际上它很差。递归函数调用自身来收集计算的所有部分结果,然后才将所有结果组合在一起。当部分结果可以累积在一个循环中时,这是一种浪费的计算方法。对于旨在运行许多或无限迭代的循环而言,这尤其成问题。
一些递归函数可以自动转换为相应的 while 循环,这些循环逐步累积总数,而不是完全累积。这种自动转换称为尾调用优化,对于使用递归进行大量循环的程序是必不可少的优化。
词法作用域
当我们的语言查找到未定义的变量时,它会抛出错误。如果能在评估程序之前告诉我们哪些变量未定义,那应当会更好。而且,这将让我们避免拼写错误和其他烦人的错误。在程序运行之前查找这些问题称为词法作用域,并使用变量定义的规则来尝试和推断哪些变量已定义,哪些完全没在程序中用上,从而不进行任何计算。
静态类型
我们程序中的每个值都要有一个相关的类型。我们在进行任何计算之前都必须确保这一点。我们的内置函数也只将某些确切的类型作为输入。我们应该能够使用此信息来推断新用户定义的函数和值的类型。在运行程序之前,我们还可以使用此信息检查是否用户使用了正确的类型调用函数。这将减少在计算之前调用具有不正确类型的函数所产生的任何错误。此检查称为静态类型。
类型系统是计算机科学中非常有趣和基本的一部分。它们是我们在运行程序之前检测错误的最佳方法。任何对编程语言安全和类型系统感兴趣的人都会发现这个项目非常有趣。