scheme编译为c

2015-05-20

overview

如何将scheme代码编译成c代码?其实只要弄清楚scheme跟c的区别,然后解决几个问题就行了。

scheme语言中是有闭包的,而c则不支持闭包。需要用closure-convert解决这个问题。

scheme语言中有call/cc,最简单的方法就是做cps变换。

cps变换之后,程序不会返回到调用者,放在c语言,栈会一直增长。如何处理这个问题?Cheney on the M.T.A.

cps-convert

做cps变换的主要目的是支持call/cc。假设输入是

(define fact (lambda (n) (if (= n 0) 1 (* n (fact (- n 1))))))

经过cps变换,它成为

(define fact (lambda (n k5980) (if (= n 0) (k5980 1) (fact (- n 1) (lambda (rv$5981) (k5980 (* n rv$5981)))))))

主要参考这篇文章。浅显易懂,混合变换很实用。

王垠的40行代码只是处理了最基本的lambda演算,而且并不好懂。跟着这篇文章的解释,才勉强能读懂这40行代码做了什么。

cps变换中需要注意的一个地方是begin的处理。如果按照参考的那篇文章做,

(T-c (begin 1 2 3) 'cont)

变换完之后会变成

(cont 3)

中间的1和2被丢弃了。在有副作用的情况下,这样子是不对的。比如

(begin (set! a 1) 2)

里面的set!就不能丢弃。

closure-convert

这位国际友人写过另一篇闭包变换的文章,不过不推荐这个实现。只需要理解其中flat closures和shared closures的概念就够了。其实作者的这篇文章中,有一个closure-convert的源代码,易读性高一些。

closure变换比cps变换要容易得多。知道它要干什么,不参考资料自己应该也能撸出来的。闭包跟c函数的区别,闭包是可以拥有自己的自由变量的 。也就是函数+环境=闭包。为了编译成c,将函数和环境拆分出来。这样函数直接对应到c的函数,环境可以用c结构体表示,而闭包则是一个包含函 数指针和环境的结构体。

用c代码表示很容易理解

struct Closure { Lambda lam ; Value env ; };

使用的时候这样调用:

struct Closure *c; c->lam(c->env, ...);

为了方便中间代码的表示,这里定义了一个closure表达式,其中一部分是一个lambda表达式,另一部分是这个闭包的环境。lambda部分接受的多了一个env参数,将env传给这个参数。

上一步cps变换后的代码,再经过closure-convert,会变成下面这个样子:

(closure (lambda (env6146 n k5980) (if (= n 0) (k5980 1) ((env-get 0 env6146) (- n 1) (closure (lambda (env6145 rv$5981) ((env-get 0 env6145) (* (env-get 1 env6145) rv$5981))) (env-make 2 k5980 n))))) (env-make 1 fact))

explicit-allocation

转换为c之后,变量是在栈上分配的。explicit-allocation将空间分配的操作也提取出来。转换后的语法中,加入了一个locate表达式。

(locate ((变量名1 类型 大小) (变量名1 类型 大小)) ...)

在转换后的中间代码语法中,我加入了closure,locate。这种自定义语法的方法在nanopass中早已领教过了。换作其它语言就没法这么玩,lisp语言的天然优势呢!

比如说:

(env-make 1 fact)

经过explicit-allocation变换之后,将成为:

(locate ((tmp8528 ENV 2)) (InitEnv tmp8529 1 fact))

这样做有什么好处呢?采用这个中间代码表示会使得生成c代码的时候特别方便。上面这段代码直接可以转换成c代码的这种写法:

struct Env tmp8528; // 栈上分配空间 InitEnv(&tmp8528, 1, fact); // 初始化

之前的fact的代码经过explicit-allocation处理之后,如下所示:

(locate ((tmp8526 CLOSURE 2) (tmp8529 ENV 1)) (InitClosure tmp8526 (lambda (env6146 n k5980) (locate ((tmp8527 CLOSURE 2) (tmp8528 ENV 2)) (if (= n 0) (k5980 1) ((env-get 0 env6146) (- n 1) (InitClosure tmp8527 (lambda (env6145 rv$5981) ((env-get 0 env6145) (* (env-get 1 env6145) rv$5981))) (InitEnv tmp8528 2 k5980 n)))))) (InitEnv tmp8529 1 fact)))

Cheney on the M.T.A

经过cps变换过的代码,是不会返回的。转换成c以后,栈会不停地增长。为了处理这个问题,要用到Cheney on the M.T.A这个算法。

基本原理就是使用垃圾回收将被使用到的数据收集起来。然后利用setjmp/longjmp跳转来回收栈空间。具体的可以用这个关键字自己去搜索。

GC

这篇文章中描述了GC的过程。其实算法特别简单,用的是一个扫描复制的垃圾回收算法。

但是我实现的时候倒是写得好痛苦。主要是调试很花精力。就单说一个小细节,下面两种表示,哪一种更好呢?

struct Vector { int size; Value v[]; };

struct Vector { int size; Value *v; };

前一种是在结构体中分配数据的空间。后一种是用的指针,指向的数组空间在别处分配。写过c代码的可能喜欢用前一种。但是这里用前一种做法是有问题的。为什么呢?因为vector可能会resize的,而前一种写法在resize之后对象地址会变。这是不可接受的,比如某个cons中可能有引用着这个Vector呢!

采用了后一种表示。在GC的时候,拷贝Vector对象单拷贝结构体是不够的,需要把指针的数组也拷贝过去。我在这个地方就被坑了,调试了好久才发现问题。

在栈上分配动态大小的空间可以用alloca函数做。反正经过cps变换之后函数是不会直接返回的,所以放心的使用栈空间。

desugar

desugar应该是在最早做的。要做的事情就是处理语法糖。

在写GC的时候,才意识到MinorGC中不扫描全局对象,那么在栈上为全局变量分配空间是不行的。看下这个代码片断:

Value fact; // 全局变量

void __lambda_tmp12() { struct Closure tmp1321; fact = InitClosure(&tmp1312, ...); }

fact的空间在栈上分配,在GC的时候这块空间被移到堆上去了。但是fact变量并不会被修改到指向新的空间。

为了处理这种情况,我想要在desugar中去掉全局变量。

(define fact ...) (fact 5)

可以改写为

((lambda () (define fact ...) (fact 5)))

再接着做一个desugar过程,内部define提升到参数中,可以变换成

((lambda (fact) (set! fact ...) (fact 5)))

但是这样做有什么问题呢?修改之后的函数的参数个数不知道该如何填充了。比如原来

(define f (lambda (n) ...)) (f 3)

如果变换后,该填充几个参数?是调用(f 3)呢?还是(f 3 '())?或者(f 3 '() '() '())

还有代码生成那边一些东西,就不说了。这里是我写的一些零零碎碎的代码片断。

只做了这些吧。验证一个东西和实现其中一些技术要点是很好玩的,但是要做完善和处理许多小细节就非常坑了。

schemecpsclosure-convert

HNS.to is a highly insecure way of browsing Handshake domains and should only be used for demo or educational purposes. Click to see preferable resolutions methods