跳转到内容

项目:一种编程语言

求值器,它决定了编程语言中表达式的含义,它只是另一个程序。

—— Hal Abelson 和 Gerald Sussman,《计算机程序的结构和解释》

chapter_picture_12.jpg

构建自己的编程语言出奇地容易(只要你的目标不要太高)而且非常有启发性。

本章我主要想说明的是,编程语言的构建并没有什么神奇之处。我经常觉得,人类的某些发明太过聪明和复杂,我永远无法理解。但只要稍加阅读和尝试,就会发现它们其实很平凡。

我们将构建一种名为 Egg 的编程语言。它将是一种小型、简单的语言,但功能强大,足以表达您能想到的任何计算。它将允许基于函数进行简单的抽象。

解析

编程语言最直观的部分是其语法或符号。解析器是一种程序,它读取一段文本并生成反映该文本中包含的程序结构的数据结构。如果文本不构成有效的程序,解析器应该指出错误。

我们的语言将具有简单而统一的语法。 Egg 中的一切都是表达式。 表达式可以是绑定的名称、数字、字符串或应用程序。 应用程序用于函数调用,也用于诸如 if 或 之类的构造 while。

为了使解析器保持简单,Egg 中的字符串不支持任何反斜杠转义之类的操作。字符串只是用双引号括起来的一系列非双引号字符。数字是一系列数字。绑定名称可以由任何非空格且在语法中没有特殊含义的字符组成。

应用程序的编写方式与 JavaScript 相同,即在表达式后放置括号,并在括号之间放置任意数量的参数,并用逗号分隔。

do(define(x, 10),
if(>(x, 5),
print("large"),
print("small")))

Egg 语言的统一性意味着 JavaScript 中的运算符(例如>)是该语言中的正常绑定,就像其他函数一样应用。由于语法没有块的概念,因此我们需要一个 do 结构来表示按顺序执行多项操作。

解析器用来描述程序的数据结构由表达式对象组成,每个表达式对象都有一个 type 属性,指示其表达式的类型,以及其他属性来描述其内容。

类型的表达式”value”表示文字字符串或数字。它们的 value 属性包含它们所表示的字符串或数字值。类型的表达式”word”用于标识符(名称)。此类对象具有 name 将标识符的名称保存为字符串的属性。最后,“apply”表达式表示应用程序。它们具有 operator 引用正在应用的表达式的属性,以及 args 保存参数表达式数组的属性。

前一个程序的部分内容 >(x, 5) 可以表示如下:

{
type: "apply",
operator: {type: "word", name: ">"},
args: [
{type: "word", name: "x"},
{type: "value", value: 5}
]
}

这种数据结构称为语法树。如果将对象想象成点,将对象之间的链接想象成这些点之间的线,如下图所示,该结构具有树状形状。表达式包含其他表达式,而这些表达式又可能包含更多表达式,这一事实类似于树枝分裂和再次分裂的方式。

syntax_tree

与我们在第 9 章中为配置文件格式编写的解析器进行对比,该解析器具有简单的结构:它将输入拆分为行并一次处理一行。一行只允许有几种简单的形式。

这里我们必须找到一种不同的方法。表达式不分成几行,并且具有递归结构。应用表达式包含其他表达式。

幸运的是,通过编写一个以反映语言递归特性的方式递归的解析器函数,可以很好地解决这个问题。

我们定义一个以字符串为输入的函数 parseExpression。它返回一个对象,其中包含字符串开头的表达式的数据结构,以及解析此表达式后剩下的字符串部分。解析子表达式(例如,应用程序的参数)时,可以再次调用此函数,生成参数表达式以及剩余的文本。此文本可能包含更多参数,也可能是结束参数列表的右括号。

这是解析器的第一部分: