目录
文章目录
前文列表
《用 C 语言开发一门编程语言 — 交互式解析器l》
《用 C 语言开发一门编程语言 — 跨平台的可移植性》
《用 C 语言开发一门编程语言 — 语法解析器》
《用 C 语言开发一门编程语言 — 抽象语法树》
《用 C 语言开发一门编程语言 — 异常处理》
使用 S-表达式进行重构
所谓 S-表达式(Symbolic Expression,S-Expression,符号表达式)是指一种以人类可读的文本形式表达半结构化数据的数学标记语言。S-Expression 在 Lisp 家族的编程语言中被应用而为人所知。S-Expression 在 Lisp 中既用作代码,也用作数据,这使得它非常强大,能完成许多其他语言不能完成的事情。
为了拥有这个强大的特性,Lisp 将运算求值过程分为两个过程:
- 先存储:读取并存储输入。
- 再求值:对输入进行求值。
而不再是简单的 “输入 -> 运算 -> 输出”。
读取并存储输入
实现 S-Expression 语法解析器
首先实现 S-Expression 的读取,我们将原有的波兰表达式解析器改造为 S-Expression 解析器。
S-Expression 的语法规则非常简单:
- 小括号之间包含一组表达式
- 表达式可以是数字、操作符或是其他的 S-Expression
根据以往实现波兰表达式解析器的经验,我们再实现了 S-Expression 解析器。另外,为了方便后续的演进,我们还把 Operator(操作符)规则重定义为了 Symbol(符号)规定,使其可以表示更多的含义,例如:操作符、变量、函数等。
mpc_parser_t* Number = mpc_new("number");
mpc_parser_t* Symbol = mpc_new("symbol");
mpc_parser_t* Sexpr = mpc_new("sexpr");
mpc_parser_t* Expr = mpc_new("expr");
mpc_parser_t* Lispy = mpc_new("lispy");
mpca_lang(MPCA_LANG_DEFAULT,
"
number : /-?[0-9]+/ ;
symbol : '+' | '-' | '*' | '/' ;
sexpr : '(' <expr>* ')' ;
expr : <number> | <symbol> | <sexpr> ;
lispy : /^/ <expr>* /$/ ;
",
Number, Symbol, Sexpr, Expr, Lispy);
mpc_cleanup(5, Number, Symbol, Sexpr, Expr, Lispy);
实现 S-Expression 存储器
为了存储输入的 S-Expression,我们需要在 Lisp 内部构建一个 **列表(数组)**结构,使其能够递归地表示数字、操作符号以及其他的列表。这一点,我们通过改造 lval 结构体来实现,并围绕器实现一系列的构造函数以及析构函数来完成。
首先,我们在表示 lval 类型的枚举变量中添加两个新的类型,分别表示 Symbols 和 S-Expression:
- LVAL_SYM:表示输入的符号,包括操作符,例如 + 等。
- LVAL_SEXPR:表示 S-Expression。
enum { LVAL_ERR, LVAL_NUM, LVAL_SYM, LVAL_SEXPR };
我们知道 S-Expression 肯定是一个可变长度的列表,而且结构体数据类型本身确实不可变长的,所以我们采用 结构体指针成员 的方式来突破这一局限。为 lval 结构体创建一个 cell 成员,cell 成员为一个二重指针类型,即:cell 本身是指针,指向数组结构的首元素,而这个数组又是一个指针数组 * lval[]
,指针数据存放着若干个(可变长)指向 lval 结构体变量的指针。这样就可以构成一个可变长的、具有父子节点结构的树状数据结构。
另外,我们还需要知道 cell 数组中所包含的元素个数,所以创建了 count 字段。
typedef struct lval {
int type;
long num;
/* Error and Symbol types have some string data */
char* err;
char* sym;
/* Count and Pointer to a list of "lval*" */
int count;
struct lval** cell;
} lval;
为了让 lval 结构体可以直接存储具体的错误信息,而不只是一个错误代码,所以还增加了 char *err
字符串用来存储错误信息,这样我们就可以把之前编写的错误类型枚举变量删除掉了。另外,我们还需要使用一个 char *sym
字符串来表示 Symbols。
实现 lval 变量的构造函数
我们知道 C 函数实参的传入采用的是值传递,即传入一个数据的时候回发生 拷贝 动作,这一特性使得向 C 函数传入大型数据结构时性能低下,解决这一问题的好方法无疑是采用 指针形参 了。所以,我们重写以往的 lval 结构体类型变量的构造函数,让其返回 lval 结构体类型指针,而不再是整个变量。
这个问题在 Python 这类 引用语义 编程语言中不会存在,因为 Python 中的变量本质就是一个指针,指向实际的数据值,所以看似像函数传入了变量,实际只是传入了地址。在 C 语言这类 值语义 编程语言中,就需要显示的实现 指针类型形参 了,本质是一样的。
为此,我们使用 malloc 库函数以及 sizeof 运算符为 lval 结构体在堆上申请足够大的内存区域,然后使用 ->
操作符填充结构体中的相关字段。
/* Construct a pointer to a new Number lval */
lval* lval_num(long x) {
lval* v = malloc(sizeof(lval));
v->type = LVAL_NUM;
v->num = x;
return v;
}
/* Construct a pointer to a new Error lval */
lval* lval_err(char* m) {
lval* v = malloc(sizeof(lval));
v->type = LVAL_ERR;
v->err = malloc(strlen(m) + 1);
strcpy(v->err, m);
return v;
}
/* Construct a pointer to a new Symbol lval */
lval* lval_sym(char* s) {
lval* v = malloc(sizeof(lval));
v->type = LVAL_SYM;
v->sym = malloc(strlen(s) + 1);
strcpy(v->sym, s);
return v;
}
/* A pointer to a new empty Sexpr lval */
lval* lval_sexpr(void) {
lval* v = malloc(sizeof(lval));
v->type = LVAL_SEXPR;
v->count = 0;
v->cell = NULL;
return v;
}
注:
- NULL 是什么?NULL 是一个指向内存地址 0 的特殊常量。按照惯例,它通常被用来表示空值或无数据。
- 什么要使用
strlen(s) + 1
?在 C 语言中,字符串的本质是一个以空字符做为终止标记的字符数组,所以,C 语言字符串的最后一个字符一定是