纸上得来终觉浅,绝知此事要躬行。
最近看了 SICP,其第四章讲述了一个简单的 Scheme 解释器的实现。粗看了一遍后决定自己用 C 语言实现一个残疾的 Scheme 解释器,想来这样的学习效果应该比只看书要强得多。
在这过程中发现用 C 实现这样一个解释器比用 Lisp 写要麻烦得多。一个 Lisp 语句本身其实就是一个 Lisp 里的表,利用这一特性就可以省去词法分析划分 token 的步骤,直接用自带的 car
和 cdr
等过程就能方便地提取出语句中的各个成分(相当于直接对树操作);而用 C 就只能老老实实地处理字符串,从而生成一个树结构。
我瞎 YY 出来的这种树结构有点 AST 的样子,暂且把它叫做语法树吧:
struct _Node {
char *exp;
struct _Node *next;
struct _Node *child;
};
next
代表右边的兄弟节点,child
则是子节点。对于复杂语句(也就是能够继续往下划分的节点)不需要记录其表达式字符串(因为并没有什么卵用),而对于 atom (也就是不能继续划分的元素)则用 exp
记录其字符串。
对于这样一个语句((lambda (x) (+ x 1)) 2)
,对应的语法树应该是这样的:
然后就是 eval
和 apply
的互相调用。
Scheme 有个特点是把函数作为一级对象,也就是和数据同等对待,所以设计 eval
函数的时候应该考虑到其可以返回数值和函数(当然还有符号和表)。于是应该设计一个 Value
结构体,使得其代表一种通用的值类型,既可以存储数值也可以存储过程。
eval
的实现中关键的部分在于判断语句的类型,这里的一大串 if..else
的顺序非常重要,大体框架应该是这样的:
if (node->child) {
node = node->child;
if (isPrimitive(node->exp)) {
return evalPrimitive(node, env);
} else if (isLambda(node->exp)) {
if (node->exp)
return evalLambda(node, env);
} else { //apply procedure
return apply(node, env);
}
} else { //self-evaluating expressions
if (isNumber(node->exp)) {
return (Value){NUMBER, atoi(node->exp)};
} else {
return getValue(node->exp, env);
}
}
其逻辑概括起来就是对于复杂表达式应该先判断其语句类型(Primitive 表示 Scheme 原有的那些操作比如算术操作),如果不属于任何类型则必然是对函数的执行;而除了复杂表达式剩下的就是自求值表达式,判断是字面量还是变量,并做相应处理。
这里涉及到求值所在的环境。环境可以用一个个嵌套的框架来表示,表示不同的作用域。每个框架有一个 parent
指针指向外围框架。对变量的定义、赋值和取值都通过维护环境框架中的一个变量列表来实现。
而 apply
函数则为函数的执行创造一个环境框架,并将形参与实参进行对应。然后再调用 eval
在新的环境里对函数体求值。
然后一个基本的 Scheme 解释器就完成了:
这个项目托管在 GitHub 上: http://github.com/lsdsjy/licp
目前只实现了 lambda
和一些关于整数的 Primitive 操作,而 define
和条件语句尚待实现。期待最后做出一个 IDE。可以预见又是一个大坑。