C宏元编程:编译期LISP解释器(二)列表操作

目录⇣
(一)总体思路
(二)列表操作
外部链接⇣
这是一个超级神奇的项目CSP Git Repo
纯粹用C宏写的LISP解释器!
(目前还没有完成,最重要的lambda已经实现了,cond暂时还有问题嵌套会出错x)
(想拉一些小伙伴一起玩一起烧脑呀!可惜似乎人类玩家直接看源码大概率大脑爆栈,于是尝试写了一些文章之类。。原始wiki可以戳这里
CSP Wiki

这次要开始分析真正的Interpreter A原语啦!坐稳啦!

列表结构

开始分析LISP操作前我们先来看看CSP中列表的表示:

(a) //我是原子
( (a) (b) (c) (d) ) //我是列表

为什么要这样呢,用下面这种方式不会更自然吗:

a //我才是原子
(a,b,c,d) //我才是列表

首先第一种方式括号更多,更符合LISP书写传统(划掉

最重要的是第一种方式在迭代列表和安全操作上有很多优点,例如迭代列表我们可以这样实现:

#define sth(x) dosth(x) sth_y
#define sth_y(x) dosth(x) sth
sth(a)(b)(c)(d) // => dosth(a) sth_y(b)(c)(d) ... 
// => dosth(a) dosth(b) ... sth 或 sth_y (最后剩下一个没展开完的)

最后剩下的那个“尾巴”可以用零点构造或者以下方式“吃掉”:

#define _sth(list) CAT(sth list,_end)
#define sth_end
#define sth_y_end
_sth((a)(b)(c)(d)) // => CAT( dosth(a) ... sth,_end) => dosth(a) ... sth_end => dosth(a)

不过零点构造技术在柯里化的多元迭代函数上有更多优点,所以CSP中两者皆有采用。

至于安全性,接下来会提到。

基本操作:CAR

CAR:取一个列表第一个元素的操作,LISP基本原语之一

以下是朴素的CAR实现:

#define CAR(x) (_CAR x ))
#define _CAR(x) x _n(
CAR ( (a) (b) (c) ) //=>(_CAR (a) (b) (c) )) 
//=> (a _n( (b) (c) )) => (a)

这里让CAR定义式中的_CAR展开出一个_n((未匹配左括号),和后面CAR中的未匹配右括号配对,构成一个零宏,从而将第一个元素后的内容都吃掉。

看起来很好是吗?不过实际上完全不能用。因为在CPP中,由于似乎无法实现短路的逻辑判断,条件分支中所有的clause都会先求值再遴选,这样这些clause大部分都会接受非法输入。

那么看看上面的CAR接受非法输入会发生什么:

CAR(a) //=>(_CAR a)) 破坏括号平衡!
CAR() //=>(_CAR )) 破坏括号平衡!

会将整个展开过程破坏掉!所以我们需要在非法输入下安全的CAR宏。

实现如下:

#define COND_EAT(x) COND_EATY
#define COND_EATY(x) COND_EAT
#define COND_EAT_FIRST(x) COND_EATY
#define SAFE_CAR_N(x) (())_n
#define SAFE_CAR_EAT_CAR )SAFE_CAR_N((
#define COND_EATY_SCEND (a)
#define COND_EAT_SCEND (a)
#define SAFE_CAR(a) _E(_e _SAFE_CAR(a))
#define _SAFE_CAR(a)  _e(_n _n()(CAT(SAFE_CAR_EAT,_E(_e CAR(CAT(COND_EAT_FIRST a,_SCEND)))))(CAR(a)))
//                                                        ^ 对于非法输入展开失败,输出 (_CAR ... ))
//                                                 ^ _E(_e ...) (两个单位宏名,一对括号)会吞掉参数的一对括号. => _CAR )
//                                 ^ 连接成 SAFE_CAR_EAT_CAR =========\
//                                v 非法输入导致最后那个CAR同样展开失败.   |
// => _e(_n _n() () SAFE_CAR_N(() (_CAR ... )) )                      <==/
// => _e( (()) )
// => (()) 
//代入 SAFE_CAR 中=> _E(_e (()) )=>()输出一个合法的空列表!

解释一下。_SAFE_CAR 宏大体分为前面的判断体_n _n()(CAT(SAFE_CAR_EAT,_E(_e CAR(CAT(COND_EAT_FIRST a,_SCEND))))) 和后面的主体(CAR(a)),以及最外面增加一次扫描次数的单位宏。判断体应当在输入非法时吃掉主体,而合法时自身输出空。

首先看到 CAT(COND_EAT_FIRST a,_SCEND),这个目的是把所有形如(b)((c)(d))...之类的合法a值约化为(a)以方便判断体逻辑(否则可能会有很多嵌套列表,难以操作),而将不合法输入约化为一个不含括号的字符串。

此后交由CAR,对于不合法输入会输出形如(_CAR ...)),吞掉一对括号后与SAFE_CAR_EAT连成SAFE_CAR_EAT_CAR,再展开成熟悉的)SAFE_CAR_N((这种未匹配括号形式影响展开过程(多出一个左括号是为了和后面CAR展开失败输出的多余右括号匹配)。而对于合法输入,则直接被_n _n() (...) 吞掉。

SAFE_CDR采用类似思路实现。

好累啊就主要部分先写这么多吧,接下来稍微扯一下CSP解释器A中怎么处理多元函数的

CSP解释器A中的柯里化

之前提过的两个宏交替展开非常好,但似乎无法处理需要两个参数的do_sth。

其实可以通过柯里化解决,不过这样展开次数始终会缺一次所以还是得外置一组单位宏来延迟展开。

柯里化技巧的核心:ZIP宏

#define _be(y) y)
#define ZIP(x)   _n() (x,_be //This _n() delays the expansion of do_sth macro, after _be is expanded. otherwise it will an unmatched bracket error. 
do_sth ZIP(a)(b) =>do_sth _n()(a,_be(b)

如果外面再加一个单位宏强制增加一次扫描,就会变成:

do_sth (a,b)

这样,一个签名为do_sth(a,b)的二元宏,就可以通过ZIP封装为do_sth ZIP,一个接受一元输入,输出一个一元宏的宏,从而能够处理一个CSP列表的前两项。

在CSP实现中,通常将参数放在待迭代列表前:

(arg)(a)(b)(c)....

do_sth实现为处理(arg,a),并将arg放在处理结果之后,然后进行递归:

do_sth (arg)
=> real_do_sth(arg,a) do_sth_y (arg) 
do_sth (arg)(a)(b)(c)
=> real_do_sth(arg,a) do_sth_y (arg) (b)(c)

这样就实现了带参数的迭代列表操作。

文章被以下专栏收录