C宏元编程:编译期LISP解释器(一)总体思路

这是一个超级神奇的项目 CSP GIt Repo

对!纯粹用C宏-那个只支持字符串替换和粘贴的东西-写的LISP解释器!

(目前还没有完成,最重要的lambda已经实现了,cond暂时还有问题嵌套会出错x)

(想拉一些小伙伴一起玩一起烧脑呀!可惜似乎人类玩家直接看源码大概率大脑爆栈,于是尝试写了一些文章之类。。原始wiki可以戳这里 CSP Wiki

那么现在就开始玩转(abuse)C宏定义的神奇(ドM)之旅吧!

总体思路

C宏定义想必大家都熟悉,姑且展开最天马行空的想象,进行LISP的列表操作(CAR CDR CONS之类)应该是可行的,First-class function某种程度上似乎也可行(传递宏名),比如这个例子:

#define E(...) __VA_ARGS__ //单位宏
#define N(...) //零宏
#define __test(something,k,...) k(do something)
#define _test(something,...) __test(something,E)
_test(testit) // => __test(testit,E) => E(do testit) => do testit
_test(E(N,N)) // => __test(N,N,E) => N(do N) =>
//_test(E(N,N))的输出消失了!

这个例子中我们看到我们将零宏和单位宏作为参数传入,并使得_test(封装 __test)具有一个零点 E(N,N)。这个技巧(私货!)在CSP的实现中经常用到!记笔记!

但是要实现lambda。。似乎纯粹的C宏很难做到(比如要把参数代入匿名函数体。。咋整啊)。。

不过别忘了神奇的LISP是可以实现自解释的!也就是可以用没有lambda语义的LISP解释器来实现完整的LISP解释器。

CSP的实现就按照了这个思路,首先实现解释器A(其实就是加载了一组实现LISP原语宏的C预处理器),然后再在解释器A上实现完整的LISP自解释器B。以下是自解释器的源码(不是最新的x 最新的在纠结那个有问题的cond)

//Line 176, csp.h
#define $zipped_eval(e,a) COND(\
        (ATOM e (EVAL_e $zipped_assoc(e,a)))    \
        (ATOM SAFE_CAR e \
         COND(($eq(SAFE_CAR e (quote))EVAL_e(SAFE_CAR SAFE_CDR e))  \
              ($eq(SAFE_CAR e (atom)) (EVAL_e ATOM\
              DELAY_INT_23($zipped_eval_R)() (SAFE_CAR SAFE_CDR e,a))) \
              ($eq(SAFE_CAR e (eq)) (EVAL_e $eq( DELAY_INT_25($zipped_eval_R)()\
              (SAFE_CAR SAFE_CDR e,a) DELAY_INT_25($zipped_eval_R)() \
              (SAFE_CAR SAFE_CDR SAFE_CDR e,a)))) \
              ($eq(SAFE_CAR e (car)) (EVAL_e SAFE_CAR DELAY_INT_23($zipped_eval_R)()\
              (SAFE_CAR SAFE_CDR e,a))) \
              ($eq(SAFE_CAR e (cdr)) (EVAL_e SAFE_CDR DELAY_INT_23($zipped_eval_R)()\
              (SAFE_CAR SAFE_CDR e,a))) \
              ($eq(SAFE_CAR e (cons)) (EVAL_e CONS DELAY_INT_23($zipped_eval_R)()\
              (SAFE_CAR SAFE_CDR e,a) DELAY_INT_23($zipped_eval_R)()\
              (SAFE_CAR SAFE_CDR SAFE_CDR e,a))) \
              ((T)(EVAL_e DELAY_INT_23($zipped_eval_R)()\
               (($zipped_assoc(SAFE_CAR e,a) EVAL_e SAFE_CDR e),a))) \
                 )                                                      \
                )                                                       \
        ($eq(SAFE_CAR SAFE_CAR e (lambda))\
        (DELAY_INT_26(EVAL_e_R)() DELAY_INT_23($zipped_eval_R)()(\
        EVAL_e(EVAL_e(EVAL_e(EVAL_e(SAFE_CAR SAFE_CDR SAFE_CDR SAFE_CAR e)))),\
        EVAL_e(APPEND DELAY_INT_13($pair_R)()(EVAL_e(EVAL_e(EVAL_e(SAFE_CAR SAFE_CDR SAFE_CAR e)))\
        (DELAY_INT_19($zipped_evlis_R)()(EVAL_e(_e EVAL_e(SAFE_CDR e)), a)))a)))\
        )                                                               \
)

是不是似曾相识啊23333

典型的自解释器实现。大家注意到这里:

1)有很多SAFE_XXX之类的东西,将来会解释。(C宏处理似乎难以实现短路condition,导致经常会对非法表达式进行求值,这种情况下使用naive的原语宏是会出事的-原地报错/破坏括号平衡-所以必须要很麻烦地一个一个实现为对于非法输入仍能给出合法结果的宏)

2)很多EVAL_e。这是一个单位宏,单位宏和零宏在C宏展开中作用非常大,因为可以用它们微调宏的展开顺序:

#define _CAT(x,y) x##y
#define CAT(x,y) _CAT(x,y) //典型的连接
//_e是一个单位宏,_n是一个零宏
#define b() )x(
_n(b()) //先展开_n再展开b,什么都没有了x
_e(_n _n()(b())) // =>_n()x() => x()
//先展开b再展开_n

因为CPP展开_e时对其括号内字符串调用展开过程(所以单位宏可以用来增加一次展开扫描过程),这个时候b会被展开,但第一个_n后面没有左括号不会展开,所以会先展开中间的_n(),下一次扫描时才第一个_n和左括号连起来被展开。

(是不是有些tricky?这是Lv0啦~等不及的可以自己看一下csp.h 2333如果能全部看懂帮我写文章吧qwq)

3) DELAY_INT_xxx(xxx_R)()(...) 这个是著名的延迟展开技巧,DELAY_INT_xxx定义如下:

#define DELAY_REF(x) x _n()
#define DELAY_REF_2(x) x _n()
#define DELAY_INT_2(x) DELAY_REF(DELAY_REF_2)(x)
#define DELAY_INT_3(x) DELAY_REF(DELAY_INT_2)(x)
#define DELAY_INT_4(x) DELAY_REF(DELAY_INT_3)(x)
#define DELAY_INT_5(x) DELAY_REF(DELAY_INT_4)(x)
...

结合2)容易理解这样 DELAY_INT_n(xxx_R)() 每次扫描n会减一,直到“露出” xxx_R与后面的()结合,习惯上定义:

#define xxx_R() xxx

这样xxx就被展开出来与参数列表结合,起到任意调节宏展开顺序的作用。

此外这个还被用于宏的递归。由于蓝色集合的限制,像这样的宏是不能实现递归的:

#define I_want_recursion(x) do_it(x) I_want_recursion(x)
I_want_recursion(x) //=>do_it(x) I_want_recursion(x)
//CPP认为它属于已经处理过的字符串于是就不会再展开了^
_e(I_want_recursion(x))//=>do_it(x) I_want_recursion(x)
//扫描多少遍都没有用!

但是可以这样:

#define I_want_recursion_R() I_want_recursion
#define I_want_recursion(x) do_it(x) DELAY_REF(I_want_recursion_R)()(x)
I_want_recursion(x) //=>do_it(x) I_want_recursion_R()(x)
_e(I_want_recursion(x)) //=>do_it(x) do_it(x) I_want_recursion_R()(x)
//因为这次I_want_recursion是由I_want_recursion_R()新造出来的token所以可以继续展开下去

不过这样需要在外面套足够多的单位宏来进行足够次数的扫描。。。CSP中有一部分使用零点构造技术更优雅地实现了递归,将来会写的x


这次就先写这些吧,下次写解释器A的列表操作实现和Currying?

文章被以下专栏收录