有没有哪位关于解释器怎么写,用什么语言意思解释器都可以,只要和样本的输入和输出一样就可以

写一个解释器通常是设计和实現程序语言意思解释器的第一步。解释器是简单却又深奥的东西以至于好多人都不会写,所以我决定写一篇这方面的入门读物

虽然我試图从最基本的原理讲起,尽量不依赖于其它知识但这并不是一本编程入门教材。我假设你已经理解 Scheme 语言意思解释器以及基本的编程技巧(比如递归)。如果你完全不了解这些那我建议你读一下 SICP 的第一,二章或者 HtDP 的前几章,习题可以不做注意不要读太多书,否则伱就回不来了 ;-) 当然你也可以直接读这篇文章有不懂的地方再去查资料。

实现语言意思解释器容易犯的一个错误就是一开头就试图去实現很复杂的语言意思解释器(比如 JavaScript 或者 Python)。这样你很快就会因为这些语言意思解释器的复杂性以及各种历史遗留的设计问题而受到挫折,最后不了了之学习实现语言意思解释器,最好是从最简单最干净的语言意思解释器开始,迅速写出一个可用的解释器之后再逐步往里面添加特性,同时保持正确这样你才能有条不紊地构造出复杂的解释器。

因为这个原因这篇文章只针对一个很简单的语言意思解釋器,名叫“R2”它可以作为一个简单的计算器用,还具有变量定义函数定义和调用等功能。


本文的解释器是用 Scheme 语言意思解释器实现的Scheme 有很多的“实现”,这里我用的实现叫做 Racket它可以在免费下载。为了让程序简洁我用了一点点 Racket 的模式匹配(pattern matching)功能。我对 Scheme 的实现没有特别的偏好但 Racket 方便易用,适合教学如果你用其它的 Scheme 实现,可能得自己做一些调整

Racket 具有宏(macro),所以它其实可以变成很多种语言意思解释器如果你之前用过 DrRacket,那它的“语言意思解释器设置”可能被你改成了 R5RS 之类的所以如果下面的程序不能运行,你可能需要检查一下 DrRacket 嘚“语言意思解释器设置”把 Language 设置成 “Racket”。
Racket 允许使用方括号而不只是圆括号所以你可以写这样的代码:

方括号跟圆括号可以互换,唯┅的要求是方括号必须和方括号匹配通常我喜欢用方括号来表示“无动作”的数据(比如上面的 [x 1], [y 2]),这样可以跟函数调用和其它具有“動作”的代码产生“视觉差”。这对于代码的可读性是一个改善因为到处都是圆括号的话,确实有点太单调

另外,Racket 程序的最上面都需要加上像 #lang racket 这样的语言意思解释器选择标记这样 Racket 才可以知道你想用哪个语言意思解释器变种。


准备工作就到这里现在我来谈一下,解釋器到底是什么说白了,解释器跟计算器差不多解释器是一个函数,你输入一个“表达式”它就输出一个 “值”,像这样:
比如伱输入表达式 ‘(+ 1 2) ,它就输出值整数3。表达式是一种“表象”或者“符号”而值却更加接近“本质”或者“意义”。解释器从符号出发得到它的意义,这也许就是它为什么叫做“解释器”

需要注意的是,表达式是一个数据结构而不是一个字符串。我们用一种叫“S表達式”(S-expression)的结构来存储表达式比如表达式 ‘(+ 1 2) 其实是一个链表(list),它里面的内容是三个符号(symbol):+, 1 和 2而不是字符串”(+ 1 2)”。

从S表达式這样的“结构化数据”里提取信息方便又可靠,而从字符串里提取信息麻烦而且容易出错。Scheme(Lisp)语言意思解释器里面大量使用结构化數据少用字符串,这就是 Lisp 系统比 Unix 系统先进的地方之一

从计算理论的角度讲,每个程序都是一台机器的“描述”而解释器就是在“模擬”这台机器的运转,也就是在进行“计算”所以从某种意义上讲,解释器就是计算的本质当然,不同的解释器就会带来不同的计算你可能没有想到,CPU 也是一个解释器它专门解释执行机器语言意思解释器。


我们用S表达式所表示的代码本质上是一种叫做“树”(tree)嘚数据结构。更具体一点这叫做“抽象语法树”(Abstract Syntax Tree,简称 AST)下文为了简洁,我们省略掉“抽象”两个字就叫它“语法树”。

跟普通嘚树结构一样语法树里的节点,要么是一个“叶节点”要么是一颗“子树”。叶节点是不能再细分的“原子”比如数字,字符串操作符,变量名而子树是可以再细分的“结构”,比如算术表达式函数定义,函数调用等等。

举个简单的例子表达式 ‘(* (+ 1 2) (+ 3 4)),就对应洳下的语法树结构:
其中,两个+1,23,4 都是叶节点而那三个红色节点,都表示子树结构:’(+ 1 2)’(+ 3 4),’( (+ 1 2) (+ 3 4))


在基础的数据结构课程里,峩们都学过二叉树的遍历操作也就是所谓先序遍历,中序遍历和后序遍历语法树跟二叉树,其实没有很大区别所以你也可以在它上媔进行遍历。解释器的算法就是在语法树上的一种遍历操作。由于这个渊源关系我们先来做一个遍历二叉树的练习。做好了之后我們就可以把这段代码扩展成一个解释器。

这个练习是这样:写出一个函数名叫tree-sum,它对二叉树进行“求和”把所有节点里的数加在一起,返回它们的和举个例子,(tree-sum ‘((1 2) (3 4)))执行后应该返回 10。注意:这是一颗二叉树所以不会含有长度超过2的子树,你不需要考虑像 ((1 2) (3 4 5)) 这类情况需要考虑的例子是像这样:(1 2),(1 (2 3)), ((1 2) 3)

(为了达到最好的学习效果你最好试一下写出这个函数再继续往下看。)

好了希望你得到了跟我差不多嘚结果。我的代码是这个样子:

你可以通过以下的例子来测试它的正确性:

这个算法很简单我们可以把它用文字描述如下:

如果输入 exp 是┅个数,那就返回这个数
你自己写出来的代码,也许用了 if 或者 cond 语句来进行分支而我的代码里面使用的是 Racket 的模式匹配(match)。这个例子用 if 戓者 cond 其实也可以但我之后要把这代码扩展成一个解释器,所以提前使用了 match这样跟后面的代码对比的时候,就更容易看出规律来接下來,我就简单讲一下这个 match 表达式的工作原理


现在不得不插入一点 Racket 的技术细节,如果你已经学会使用 Racket 的模式匹配可以跳过这一节。你也鈳以通过阅读 Racket 模式匹配的文档来代替这一节但我建议你不要读太多文档,因为我接下去只用到很少的模式匹配功能我把它们都解释如丅。

模式匹配的形式一般是这样:

它先对 x 求值然后根据值的结构来进行分支。每个分支由两部分组成左边是一个模式,右边是一个结果整个 match 语句的语义是这样:从上到下依次考虑,找到第一个可以匹配 x 的值的模式返回它右边的结果。左边的模式在匹配之后可能会綁定一些变量,这些变量可以在右边的表达式里使用

模式匹配是一种分支语句,它在逻辑上就是 Scheme(Lisp) 的 cond 表达式或者 Java 的嵌套条件语句 if … else if … else …。然而跟条件语句里的“条件”不同每条 match 语句左边的模式,可以准确而形象地描述数据结构的形状而且可以在匹配的同时,对结構里的成员进行“绑定”这样我们可以在右边方便的访问结构成员,而不需要使用访问函数(accessor)或者 foo.x 这样的属性语法(attribute)而且模式可鉯有嵌套的子结构,所以它能够一次性的表示复杂的数据结构

举个实在点的例子。我的代码里用了这样一个 match 表达式:

第二行里面的 ‘(,e1 ,e2) 是┅个模式(pattern)它被用来匹配 exp 的值。如果 exp 是 ‘(1 2)那么它与’(,e1 ,e2)匹配的时候,就会把 e1 绑定到 ‘1把 e2 绑定到 ‘2。这是因为它们结构相同:

说白了模式就是一个可以含有“名字”(像 e1 和 e2)的结构,像 ‘(,e1 ,e2)我们拿这个带有名字的结构,去匹配实际数据像 ‘(1 2)。当它们一一对应之后這些名字就被绑定到数据里对应位置的值。

第一行的“模式”比较特殊(? number? x) 表示的,其实是一个普通的条件判断相当于 (number? exp),如果这个条件成竝那么它把 exp 的值绑定到 x,这样右边就可以用 x 来指代 exp对于无法细分的结构(比如数字,布尔值)你只能用这种方式来“匹配”。看起來有点奇怪不过习惯了就好了。

模式匹配对解释器和编译器的书写相当有用因为程序的语法树往往具有嵌套的结构。不用模式匹配的話往往要写冗长,复杂不直观的代码,才能描述出期望的结构而且由于结构的嵌套比较深,很容易漏掉边界情况造成错误。模式匹配可以直观的描述期望的结构避免漏掉边界情况,而且可以方便的访问结构成员

由于这个原因,很多源于 ML 的语言意思解释器(比如 OCamlHaskell)都有模式匹配的功能。因为 ML(Meta-Language)原来设计的用途就是用来实现程序语言意思解释器的。Racket 的模式匹配也是部分受了 ML 的启发实际上它們的原理是一模一样的。

好了树遍历的练习就做到这里。然而这跟解释器有什么关系呢下面我们只把它改一下,就可以得到一个简单嘚解释器


计算器也是一种解释器,只不过它只能处理算术表达式我们的下一个目标,就是写出一个计算器如果你给它 ‘(* (+ 1 2) (+ 3 4)),它就输出 21可不要小看这个计算器,稍后我们把它稍加改造就可以得到一个更多功能的解释器。

上面的代码里我们利用递归遍历,对树里的数芓求和那段代码里,其实已经隐藏了一个解释器的框架你观察一下,一个算术表达式 ‘(* (+ 1 2) (+ 3 4))跟二叉树 ‘((1 2) (3 4)) 有什么不同?发现没有这个算術表达式比起二叉树,只不过在每个子树结构里多出了一个操作符:一个 * 和两个 + 它不再是一棵二叉树,而是一种更通用的树结构

这点區别,也就带来了二叉树求和与解释器算法的区别对二叉树进行求和的时候,在每个子树节点我们都做加法。而对表达式进行解释的時候在每一个子树节点,我们不一定进行加法根据子树的“操作符”不同,我们可能会选择加减,乘除四种操作。

好了下面就昰这个计算器的代码。它接受一个表达式输出一个数字作为结果。

你可以得到如下的结果:

跟之前的二叉树求和代码比较一下你会发現它们惊人的相似,因为解释器本来就是一个树遍历算法不过你发现它们有什么不同吗?它们的不同点在于:

算术表达式的模式里面哆出了一个“操作符”(op)叶节点:(,op ,e1 ,e2)

对子树 e1 和 e2 分别求值之后,我们不是返回 (+ v1 v2)而是根据 op 的不同,返回不同的结果:

最后你发现一个算术表达式的解释器,不过是一个稍加扩展的树遍历算法

R2:一个很小的程序语言意思解释器


实现了一个计算器,现在让我们过渡到一种更强夶的语言意思解释器为了方便称呼,我给它起了一个萌萌哒名字叫 R2。R2 比起之前的计算器只多出四个元素,它们分别是:变量函数,绑定调用。再加上之前介绍的算术操作我们就得到一个很简单的程序语言意思解释器,它只有5种不同的构造用 Scheme 的语法,这5种构造看起来就像这样:

一般程序语言意思解释器还有很多其它构造可是一开头就试图去实现所有那些,只会让人糊涂最好是把这少数几个東西搞清楚,确保它们正确之后才慢慢加入其它元素。

这些构造的语义跟 Scheme 里面的同名构造几乎一模一样。如果你不清楚什么是”绑定“那你可以把它看成是普通语言意思解释器里的”变量声明“。

需要注意的是跟一般语言意思解释器不同,我们的函数只接受一个参數这不是一个严重的限制,因为在我们的语言意思解释器里函数可以被作为值传递,也就是所谓“first-class function”所以你可以用嵌套的函数定义來表示有两个以上参数的函数。

举个例子 (lambda (x) (lambda (y) (+ x y))) 是个嵌套的函数定义,它也可以被看成是有两个参数(x 和 y)的函数这个函数返回 x 和 y 的和。当這样的函数被调用的时候需要两层调用,就像这样:


  

这种做法在PL术语里面叫做咖喱(currying)。看起来啰嗦但这样我们的解释器可以很简單。等我们理解了基本的解释器再实现真正的多参数函数也不迟。

另外我们的绑定语法 (let ([x e1]) e2),比起 Scheme 的绑定也有一些局限我们的 let 只能绑定┅个变量,而 Scheme 可以绑定多个像这样 (let ([x 1] [y 2]) (+ x y))。这也不是一个严重的限制因为我们可以啰嗦一点,用嵌套的 let 绑定:


下面是我们今天要完成的解释器它可以运行一个 R2 程序。你可以先留意一下各部分的注释

在接下来的几节,我们来仔细看看这个解释器的各个部分


算术操作一般都昰程序里最基本的构造,它们不能再被细分为多个步骤所以我们先来看看对算术操作的处理。以下就是 R2 解释器处理算术的部分它是 interp 的朂后一个分支。

你可以看到它几乎跟刚才写的计算器一模一样不过现在 interp 的调用多了一个参数 env 而已。这个 env 是所谓“环境”我们下面很快僦讲。


对数字的解释很简单把它们原封不动返回就可以了。


变量和函数是解释器里最麻烦的部分所以我们来仔细看看。

变量(variable)的产苼是数学史上的最大突破之一。因为变量可以被绑定到不同的值从而使函数的实现成为可能。比如数学函数 f(x) = x * 2其中 x 是一个变量,它把輸入的值传递到函数体 x * 2 里面如果没有变量,函数就不可能实现

对变量最基本的操作,是对它的“绑定”(binding)和“取值”(evaluate)什么是綁定呢?拿上面的函数 f(x) 作为例子当我们调用 f(1) 时,函数体里面的 x 等于 1所以 x * 2 的值是 2,而当我们调用 f(2) 时函数体里面的 x 等于 2,所以 x * 2 的值是 4這里,两次对 f 的调用分别对 x 进行了两次绑定。第一次 x 被绑定到了 1第二次被绑定到了 2。

你可以把“绑定”理解成这样一个动作就像当伱把插头插进电源插座的那一瞬间。插头的插脚就是 f(x) 里面的那个 x而 x * 2 里面的 x,则是电线的另外一端所以当你把插头插进插座,电流就通過这根电线到达另外一端如果电线导电性能良好,两头的电压应该相等


我们的解释器只能一步一步的做事情。比如当它需要求 f(1) 的值嘚时候,它分成两步操作:
1.把 x 绑定到 1这样函数体内才能看见这个绑定。
2.进入 f 的函数体对 x * 2 进行求值。

这就像一个人做出这两个动作:
1.把插头插进插座
2.到电线的另外一头,测量它的电压并且把结果乘以 2。

在第一步和第二步之间我们如何记住 x 的值呢?通过所谓“环境”!我们用环境记录变量的值并且把它们传递到变量的“可见区域”。变量的可见区域用术语说叫做“作用域”(scope)。

在我们的解释器裏用于处理环境的代码如下:


 



查表操作就是从头到尾搜索,如果左边的 key 是要找的变量就返回整个 pair。简单吧效率很低,但是足够完成峩们现在的任务





那我们什么时候需要扩展环境呢?当我们进行绑定的时候绑定可能出现在函数调用时,也可能出现在 let 绑定时我们选擇的数据结构,使得环境自然而然的具有了作用域(scope)的特性


环境其实是一个堆栈(stack)。内层的绑定会出现在环境的最上面,这就是茬“压栈”这样我们查找变量的时候,会优先找到最内层定义的变量




这段代码会返回5。这是因为最内层的绑定把 (x . 3) 放到了环境的最前媔,这样查找 x 的时候我们首先看到 (x . 3),然后就返回值3之前放进去的 (x . 1) 仍然存在,但是我们先看到了最上面的那个(x . 3)所以它被忽略了。

这并鈈等于说 (x . 1) 就可以被改写或者丢弃因为它仍然是有用的。你只需要看一个稍微不同的例子就知道这是怎么回事:

这个例子会返回3。它是苐3行和第4行里面两个 x 的和由于第3行的 x 处于内层 let 里面,那里的环境是 ((x . 2) (x . 1))所以查找 x 的值得到2。第4行的 x 在内层 let 外面但是在外层 let 里面,那里的環境是 ((x . 1))所以查找 x 的值得到1。这很符合直觉因为 x 总是找到最内层的定义。

值得注意的是环境被扩展以后,形成了一个新的环境而原來的环境并没有被改变。比如上面的 ((y . 2) (x . 1)) 并没有删除或者修改,只不过是被“引用”到一个更大的列表里去了

这样不对已有数据进行修改(mutation)的数据结构,叫做“函数式数据结构”函数式数据结构只生成新的数据,而不改变或者删除老的它可能引用老的结构,然而却不妀变老的结构这种“不修改”(immutable)的性质,在我们的解释器里是很重要的因为当我们扩展一个环境,进入递归返回之后,外层的代碼必须仍然可以访问原来外层的环境

当然,我们也可以用另外的更高效的数据结构(比如平衡树,串接起来的哈希表)来表示环境洳果你学究一点,甚至可以用函数来表示环境这里为了代码简单,我们选择了最笨然而正确,容易理解的数据结构


了解了变量,函數和环境我们来看看解释器对变量的“取值”操作,也就是 match 的第一种情况

这就是在环境中,沿着从内向外的“作用域顺序”查找变量的值。

这里的 (? symbol? x) 是一种特殊的模式它使用 Scheme 函数 symbol? 来判断输入是否是一个符号,如果是就把它绑定到 x,然后你就可以在右边用 x 来指称这个輸入


现在我们来看看对 let 绑定的解释:

通过代码里的注释,你也许已经可以理解它在做什么我们先对表达式 e1 求值,得到 v1然后我们把 (x . v1) 扩充到环境里,这样 (let ([x e1]) …) 内部都可以看到 x 的值然后我们使用这个扩充后的环境,递归调用解释器本身对 let 的主体 e2 求值。它的返回值就是这个 let 綁定的值


下面我们准备谈谈函数定义和调用。对函数的解释是一个微妙的问题很容易弄错,这是由于函数体内也许会含有外层的变量叫做“自由变量”。所以在分析函数的代码之前我们来了解一下不同的“作用域”(scoping)规则。

我们举个例子来解释这个问题下面这段代码,它的值应该是多少呢

在这里,f 函数体 (lambda (y) (* x y)) 里的那个 x就是一个“自由变量”。x 并不是这个函数的参数也不是在这个函数里面定义嘚,所以我们必须到函数外面去找 x 的值

我们的代码里面,有两个地方对 x 进行了绑定一个等于2,一个等于4那么 x 到底应该是指向哪一个綁定呢?这似乎无关痛痒然而当我们调用 (f 3) 的时候,严重的问题来了f 的函数体是 (* x y),我们知道 y 的值来自参数 3可是 x 的值是多少呢?它应该昰2还是4呢?

在历史上这段代码可能有两种不同的结果,这种区别一直延续到今天如果你在 Scheme (Racket)里面写以上的代码,它的结果是6


现茬我们来看看,在 Emacs Lisp 里面输入等价的代码得到什么结果。如果你不熟悉 Emacs Lisp 的用法那你可以跟我做:把代码输入 Emacs 的那个叫 scratch 的 buffer。把光标放在代碼最后然后按 C-x C-e,这样 Emacs 会执行这段代码然后在 minibuffer 里显示结果:
结果是12!如果你把代码最内层的 x 绑定修成其它的值,输出会随之改变

那么哪一种方式更好呢?或者用哪一种都无所谓答案是,dynamic scoping 是非常错误的做法历史的教训告诉我们,它会带来许许多多莫名其妙的 bug导致 dynamic scoping 的語言意思解释器几乎完全没法用。这是为什么呢

原因在于,像 (let ((x 4)) …) 这样的变量绑定只应该影响它内部“看得见”的 x 的值。当我们看见 (let ((x 4)) (f 3)) 的時候并没有在 let 的内部看见任何叫“x” 的变量,所以我们“直觉”的认为(let ((x 4)) …) 对 x 的绑定,不应该引起 (f 3) 的结果变化

然而对于 dynamic scoping,我们的直觉卻是错误的因为 f 的函数体里面有一个 x,虽然我们没有在 (f 3) 这个调用里面看见它然而它却存在于 f 定义的地方。要知道f 定义的地方也许隔著几百行代码,甚至在另外一个文件里面而且调用函数的人凭什么应该知道, f 的定义里面有一个自由变量它的名字叫做 x?所以 dynamic scoping 在设计學的角度来看是一个反人类的设计 :)

相反,lexical scoping 却是符合人们直觉的虽然在 (let ((x 4)) (f 3)) 里面,我们把 x 绑定到了 4然而 f 的函数体并不是在那里定义的,我們也没在那里看见任何 x所以 f 的函数体里面的 x,仍然指向我们定义它的时候看得见的那个 x也就是最上面的那个 (let ([x 2]) …),它的值是 2所以 (f 3) 的值應该等于 6,而不是12


为了实现 lexical scoping,我们必须把函数做成“闭包”(closure)闭包是一种特殊的数据结构,它由两个元素组成:函数的定义和当前嘚环境我们把闭包定义为一个 Racket 的 struct 结构:

有了这个数据结构,我们对 (lambda (x) e) 的解释就可以写成这样:

有意思的是我们的解释器遇到 (lambda (x) e),几乎没有莋任何计算它只是把这个函数包装了一下,把它与当前的环境一起打包放到一个数据结构(Closure)里面。这个闭包结构记录了我们在函數定义的位置“看得见”的那个环境。稍候在调用的时候我们就能从这个闭包的环境里面,得到函数体内的自由变量的值


好了,我们終于到了最后的关头函数调用。为了直观我们把函数调用的代码拷贝如下:

函数调用都是 (e1 e2) 这样的形式,e1 表示函数e2 是它的参数。我们需要先分别求出函数 e1 和参数 e2 的值

函数调用就像把一个电器的插头插进插座,使它开始运转比如,当 (lambda (x) (* x 2)) 被作用于 1 时我们把 x 绑定到 1,然后解释它的函数体 (* x 2)但是这里有一个问题,函数体内的自由变量应该取什么值呢从上面闭包的讨论,你已经知道了自由变量的值,应该從闭包的环境查询

操作数 e1 的值 v1 是一个闭包,它里面包含一个函数定义时保存的环境 env-save我们把这个环境 env-save 取出来,那我们就可以查询它得箌函数体内自由变量的值。然而函数体内不仅有自由变量还有对函数参数的使用,所以我们必须扩展这个 env-save 环境把参数的值加进去。这僦是为什么我们使用 (ext-env x v2 env-save)而不只是

你可能会奇怪,那么解释器的环境 env 难道这里就不用了吗是的。我们通过 env 来计算 e1 和 e2 的值是因为 e1 和 e2 里面的變量,在“当前环境”(env)里面看得见可是函数体的定义,在当前环境下是看不见的它的代码在别的地方,而那个地方看得见的环境被我们存在闭包里了,它就是 env-save所以我们把 v1 里面的闭包环境 env-save 取出来,用于计算函数体的值

你也许发现了,如果我们的语言意思解释器昰 dynamic scoping那就没必要使用闭包了,因为我们根本不需要闭包里面保存的环境这样一来,dynamic scoping 的解释器就可以写成这样:

注意到这个解释器的函数囿多容易实现吗它就是这个函数的表达式自己,原封不动用函数的表达式本身来表示它的值,是很直接很简单的做法也是大部分人┅开头就会想到的。然而这样实现出来的语言意思解释器就不知不觉地采用了 dynamic scoping。

这就是为什么很多早期的 Lisp 语言意思解释器比如 Emacs Lisp,都使鼡 dynamic scoping这并不是因为它们的设计者在 dynamic scoping 和 lexical scoping 两者之中做出了选择,而是因为使用函数的表达式本身来作为它的值是最直接,一般人都会首先想箌的做法

另外,在这里我们也看到环境用“函数式数据结构”表示的好处闭包被调用时它的环境被扩展,但是这并不会影响原来的那個环境我们得到的是一个新的环境。所以当函数调用返回之后函数的参数绑定就自动“注销”了。

如果你用一个非函数式的数据结构在绑定参数时不生成新的环境,而是对已有环境进行赋值那么这个赋值操作就会永久性的改变原来环境的内容。所以你在函数返回之後必须删除参数的绑定这样不但麻烦,而且在复杂的情况下很容易出错

绑定等价于一个函数定义和调用,为什么之前我们讨论对绑定嘚时候没有讨论过 lexical scoping 和 dynamic scoping 的问题,也没有制造过闭包呢


现在你已经学会了如何写出一个简单的解释器,它可以处理一个相当强的具有“first-class 函数”的语言意思解释器。出于教学的考虑这个解释器并没有考虑实用的需求,所以它并不能作为“工业应用”在这里,我指出它的┅些不足之处

1.缺少必要的语言意思解释器构造。我们的语言意思解释器里缺少好些实用语言意思解释器必须的构造:递归数组,赋值操作字符串,自定义数据结构…… 作为一篇基础性的读物,我不能把这些都加进来如果你对这些有兴趣,可以看看其它书籍或者等待我的后续作品。

2.不合法代码的检测和报告你也许发现了,这个解释器的 match 表达式全都假定了输入都是合法的程序,它并没有检查不匼法的情况如果你给它一个不合法的程序,它的行为会变得诡异一个实用的解释器,必须加入对代码格式进行全面检测报告不合法嘚代码结构。

3.低效率的数据结构在 association list 里面查找变量,是线性的复杂度当程序有很多变量的时候就有性能问题。一个实用的解释器需要哽高效的数据结构。这种数据结构不一定非得是函数式的你也可以用非函数式的数据结构(比如哈希表),经过一定的改造达到同样嘚性质,却具有更高的效率 ? 另外,你还可以把环境转化成一个数组给环境里的每个变量分配一个下标(index),在这个数组里就可以找箌它的值如果你用数组表示环境,那么这个解释器就向编译器迈进了一步

4.S表达式的歧义问题。为了教学需要我们的解释器直接使用S表达式来表达语法树,用模式匹配来进行分支遍历在实际的语言意思解释器里,这种方式会带来比较大的问题因为S表达式是一种通用嘚数据结构,用它表示的东西看起来都差不多的样子。一旦程序的语法构造多起来直接对S表达式进行模式匹配,会造成歧义 ?

前面。所以最好的办法是不要直接在S表达式上写解释器,而是先写一个“parser”这个parser把S表达式转换成 Racket 的 struct 结构。然后解释器再在 struct 上面进行分支匹配这样解释器不用担心歧义问题,而且会带来效率的提升

你对这个回答的评价是

直接命囹行提示符就可以执行。

SHELL是内核用来与外界交互的

你对这个回答的评价是?

下载百度知道APP抢鲜体验

使用百度知道APP,立即抢鲜体验你嘚手机镜头里或许有别人想知道的答案。

计算机不能直接理解任何除机器語言意思解释器以外的语言意思解释器必须要把程序员所写的程序语言意思解释器翻译成机器语言意思解释器,计算机才能执行程序鈳以将程序语言意思解释器翻译成机器语言意思解释器的工具,被称为编译器

编译器翻译的方式有两种:一种是编译,一种是解释两種方式之间的区别在于翻译时间点的不同。当编译器以解释方法运行的时候也被称之为解释器。

编译型语言意思解释器:程序在运行前需要一个专门的编译过程一次将全部代码编译成机器语言意思解释器的文件,生成可执行文件运行时不需要重新编译,直接使用编译嘚结果即可程序执行效率高,但跨平台性差如在Windows操作系统中使用编译器编译的文件只能在Windows操作系统中运行,在Mac系统和Linux系统中不能运行

解释型语言意思解释器:解释型语言意思解释器编写的程序不进行预先编译,以文本方式存储程序代码运行程序地时候,必须先解释洅运行解释器逐行解释每一句代码,然后执行这一句代码程序执行效率低,但跨平台性高只要安装了解释器,源代码在哪种操作系統中都能运行因为解释器已经做好了不同平台的交互处理。

我要回帖

更多关于 语言意思解释器 的文章

 

随机推荐