这篇文章会介绍一下 chibi-scheme 语法分析的其他部分,因为我总觉得这其实是很有趣的部分。

从sexp中可以看到所有的ast类型的定义。可见其中比较复杂的是 lambda类型。这一次打算看一看lambda这个结构体的语法分析,因为其中牵涉到Context类型、我打算整篇博客就介绍这一个部分。

Context

Context表示的是执行所需要的状态,Context中的内容包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct {
sexp_heap heap;
struct sexp_gc_var_t *saves;
#if SEXP_USE_GREEN_THREADS
sexp_sint_t refuel;
unsigned char* ip;
struct timeval tval;
#endif
char tailp, tracep, timeoutp, waitp, errorp;
sexp_uint_t last_fp;
#if SEXP_USE_TIME_GC
sexp_uint_t gc_count, gc_usecs;
#endif
sexp stack, env, parent, child,
globals, dk, params, proc, name, specific, event, result;
#if SEXP_USE_DL
sexp dl;
#endif
} context;

可见在其间有环境、栈、堆结构;Context是执行一个sexp所需要的结构。它记录了解释器当前的状态,当有多个解释器线程同时运行的时候,每一个线程都会有一个它自己的Context结构体。这个Context结构中的(部分)变量和意义记录如下:

变量名 意义
heap 程序运行的 堆
stack 程序运行的 栈
env 当前的环境
parent 指向父Context
child 指向子Context
globals 用于保存一些全局的信息
dk 一些预先定义的函数
specific 在编译阶段使用的一个vector,其中包含:bc、fv、lambda等属性

我们可以通过C语言调用FFI的方式输出globals的内容,这个之后再进行介绍。输出的结果是一串很长很长的全局变量串。

构造一个环境变量的函数是 sexp_make_context(在sexp.c文件中)。

第一个参数ctx表示传进来一个context作为当前构造的Context的父Context。

它里面会构造一个极其简单的全局Context,这里面只有两个稍微复杂的地方:最开始部分的如果使用全局堆的时候、会复制堆的一些属性,否则会新分配一个堆;最后部分,如果没有ctx参数的时候,会初始化一个全局变量,如果有ctx参数,则会复制ctx变量的globals和dk属性。

在外面创建一个独立的上下文的时候都会使用sexp_make_context 函数,但是用这个函数产生的上下文环境虽然能用于构造并不能执行、因为它没有将Context与实际的堆栈、环境等与实际的代码关联起来。所以我们需要在外面给它套上一个实现在eval.c中的sexp_make_eval_context的函数。

这里虽然文档说它是Similar to sexp_make_context,但是源代码里面可见sexp_make_eval_context实际是直接调用sexp_make_context的。

在这个函数里对每个属性已经依次的赋值、最后得到一个完整的的Context结构体给出去。其中函数sexp_context_lambda/sexp_context_bc/sexp_context_fv等函数都是对specific里的属性进行赋值。specific是一个有七个内容的vector,主要是在编译阶段使用。其中:

  • bc属性是指字节码(bytecode),chibi-schemevm.c文件中实现了一个虚拟机,我们需要将代码转换为字节码(也就是中间代码)然后交给vm来执行。vm的主要好处是隔离了硬件,使得不必去关注硬件的实现细节、这里的种种展开这里就不提。

  • fv属性指自由变量(free variable),指的是没有在作用域中进行声明的变量。

  • lambda属性用于指定这个Context的作用域。

之后就可以使用ctx来执行语句了:Context变量是执行chibi-scheme语句的函数sexp_evalsexp_eval_string的第一个参数。

还有关于它的操作都是关于加载文件/环境/输入输出端口的。此处就略过了,详细可以参见文档的这里

lambda 类型

lambda类型结构体的定义如下:

1
2
3
struct {
sexp name, params, body, defs, locals, flags, fv, sv, ret, types, source;
} lambda;

与lambda 类型相通的是 eval.c里面的 analyze_lambda函数。正常的lambda表达式应该是类似这样的:

1
2
> (lambda (x y) (+ x y))
#<procedure #f>

(忽视gc相关的内容后)最开始的 verify syntax 内容里面对应的是下面这三种错误:

1
2
3
4
5
6
> (lambda x) ; 参数数量不对
ERROR on line 14: bad lambda syntax: (lambda x)
> (lambda (+ x 1) x) ; 第一个参数里面不是符号
ERROR on line 17: non-symbol parameter: (lambda (+ x 1) x)
> (lambda (x x) x) ; 重复使用符号
ERROR on line 2: duplicate parameter: (lambda (x x) x)

lambda结构体的定义可以在上面看到,这个结构的属性和意义(部分)如下:

sexp 意义
name lambda函数通过define绑定的名字(如果有的话)
params lambda的参数
body lambda的函数体
defs lambda中出现的define语句
fv 函数中出现的自由变量
sv 保存保护变量
ret 返回值的类型
types 参数的类型
source lambda的完整内容

source 是编译器为了支持 debug 弄的。几乎每一个ast类型都有一个source属性。

在函数中、接下来这段build lambda and analyze body 就是填充 lambda中的source、body、params部分,同时为函数构造一个新的Context结构体(作为当前Context的子Context),给结构体的env和lambda变量赋值。在这里面其实还有一个特殊的操作,也就是sexp_flatten_dot这个函数,将点后面的所有参数都解释为一个列表,使得你可以引入不定数量的参数。下面是一个例子:

1
2
3
> (define func (lambda (num . nums) nums))
> (func 1 2 3 4 5)
(2 3 4 5)

之后这段delayed analyze internal defines通过一个for循环遍历整个lambda,然后找到其中的define语句,延迟进行这些语句,获得它们的name和value,将它们都放到defs属性中。它们将随着函数的开始而执行。