├── Appendix.md ├── Basics.md ├── Bonus Projects.md ├── Conditionals.md ├── Error Handling.md ├── Evaluation.md ├── Functions.md ├── Installation.md ├── Interactive.md ├── Introduction.md ├── Languages.md ├── Parsing.md ├── Q-Expressions.md ├── README.md ├── S-Expressions.md ├── SUMMARY.md ├── Standard Library.md ├── Strings.md └── Variables.md /Appendix.md: -------------------------------------------------------------------------------- 1 | # 第十七章 • 附录 2 | 3 | ## 特别感谢 4 | 5 | 非常感谢我的朋友和家人给予我的支持,特别是 Francesca Shaw,她让我全身心地投入到写作当中, 还有 Caroline Holden,帮助我校对书籍。 6 | 7 | 另外,也感谢 Miran Lipovaca、Frederic Trottier-Hebert 以及 Jonathan Tang。他们的书籍《[Learn you a Haskell](http://learnyouahaskell.com/)》、《[Learn you some Erlang](http://learnyousomeerlang.com/)》、《[Write Yourself a Scheme in 48 Hours](http://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours)》给予了我写作的灵感。 8 | 9 | ## Beta 版本读者 10 | 11 | 谢谢所有 Beta 版本的读者提供的反馈、纠错、建议以及鼓励。非常感谢以下 Reddit 用户:neelaryan, bitsbytesbikes, acesHD, CodyChan, northClan, da4c30ff, nowords, ozhank, crackez, stubarfoo, viezebanaan, JMagnum86, uNEV6X29rpf3, fortyninezeronine, skeeto, miketaylr, wonnernaus, Barthalion, codyrioux, sigjuice, yoshiK, u-n-sky。 -------------------------------------------------------------------------------- /Basics.md: -------------------------------------------------------------------------------- 1 | # 第零三章 • 基础 2 | 3 | 这本章中,我会带你快速的浏览一遍 C 语言的基本特性(feature)。C 语言的特性非常少,而且语法也相当的简单明了。但是这并不代表 C 语言很简单,有深度的内容绝不会浮于表面。所以,接下来我们将快速地了解一下 C 语言的基础,在后面的章节中再慢慢深入。 4 | 5 | 本章的目的是让所有的读者位于同一水平线上。C 的新手可以在本章多花一些时间,而已经对 C 有一定程度了解的同学则可以大致浏览一遍或直接跳过本章。 6 | 7 | ## 程序 8 | 9 | C 语言程序是由函数定义和类型定义组成的。 10 | 11 | 因此一个源文件就是一系列的函数和类型。每个函数都可以调用其他函数或调用自身(递归),可以使用任何已经声明的或内建的数据类型。 12 | 13 | 你还可以调用其他库提供的函数,使用库中提供的数据类型,这也是 C 中多层级系统的复杂性不断增长的根源。 14 | 15 | 在前一章中,我们提到,所有的程序都是从 `main` 函数开始执行的。从 `main` 函数开始不断地调用越来越多的函数,完成期望的工作。 16 | 17 | ## 变量 18 | 19 | C 中的函数可以拥有多个变量。变量其实就是一个有名字的数据项。 20 | 21 | C 中的每个变量都有一个特定的类型。这些类型可能是内建的,也可能是我们自定义的。声明一个新变量的时候,首先需要将类型的名字写在前面,然后紧跟的是变量的名字,你还可以使用 `=` 给这个变量赋一个初始值。变量的声明是一个*语句*,在 C 语言中,所有的语句必须由 `;` 结尾。 22 | 23 | 如果要创建一个名为 `count` 的整数(`int`),则可以这样写: 24 | 25 | ```c 26 | int count; 27 | ``` 28 | 29 | 如果要给 count 赋一个初始值: 30 | 31 | ```c 32 | int count = 10; 33 | ``` 34 | 35 | 下面列出了 C 语言中一些内建的类型: 36 | 37 | |类型名|描述|举例| 38 | |-----------|-----------|-----------| 39 | |`void`|空类型|| 40 | |`char`|单个的字符/字节|`char last_initial = 'H';`| 41 | |`int`|整数|`int age = 32;`| 42 | |`long`|可以表示更大的数的整数|`long age_of_universe = 13798000000;`| 43 | |`float`|浮点数|`float liters_per_pint = 0.568f;`| 44 | |`double`|精度更高的浮点数|`double speed_of_swallow = 0.01072896;`| 45 | 46 | 47 | ## 函数声明 48 | 49 | 函数是一个针对变量的操作过程,同时可能也会改变当前程序的状态。它接受多个输入值,计算并返回一个输出值。 50 | 51 | 声明函数时,首先将返回值的类型写在前面,后面紧跟函数的名字,而后的一对圆括号里面包裹函数的输入参数,参数之间用 `,` 进行分割。函数体部分紧跟其后,包裹在一对花括号 `{}` 里,里面包含了函数执行的所有语句,语句之间使用 `;` 分隔。`return` 语句用来结束函数的执行,并返回一个值。 52 | 53 | 下面的代码演示了一个将两个 `int` 型变量 `x`,`y` 求和之后并返回的函数写法。 54 | 55 | ```c 56 | int add_together(int x, int y) { 57 | int result = x + y; 58 | return result; 59 | } 60 | ``` 61 | 62 | 调用函数时,首先写上函数名,然后函数参数紧跟其后,包裹在一对圆括号里,参数之间用逗号分开。比如说,我们调用上面的函数,并将计算结果保存到 `added` 变量中: 63 | 64 | ```c 65 | int added = add_together(10, 18); 66 | ``` 67 | 68 | ## 结构体声明 69 | 70 | 结构体用来声明一个新的类型。它可以将多个变量捆绑在一起。 71 | 72 | 我们可以使用结构体表示更加复杂的数据类型。例如,为了表示一个二维空间里的点,我们可以创建一个名为 `point` 的结构体将两个 `float` 类型的变量 `x`,`y` 绑在一起。我们可以同时使用 `struct` 和 `typedef` 来声明一个结构体: 73 | 74 | ```c 75 | typedef struct { 76 | float x; 77 | float y; 78 | } point; 79 | ``` 80 | 81 | 注意,我们应该将结构体放在所有用到它的函数的上方。这个类型和内建的基本数据类型的用法没有任何区别。获取结构体内部的变量时,需要使用小数点 `.`,后面紧跟要获取的变量名: 82 | 83 | ```c 84 | point p; 85 | p.x = 0.1; 86 | p.y = 10.0; 87 | 88 | float length = sqrt(p.x * p.x + p.y * p.y); 89 | ``` 90 | 91 | ## 指针 92 | 93 | 指针类型是普通类型的变体,只需普通类型的后面添加 `*` 后缀即可。比如,`int*` 就是一个 `int` 类型的指针。在之前的 `main` 函数的输入参数列表中,我们还见过一个 `char**` 类型,这是一个 `char` 类型的指针的指针。 94 | 95 | 指针是 C 语言的精髓所在,也是 C 语言中比较难理解的知识点,我们会在以后的章节中详细讲解。目前的阶段我们还不会用到指针,所以你只需要知道有这个东西就可以了。 96 | 97 | ## 字符串 98 | 99 | 在 C 语言中,字符串由 `char*` 类型表示。它是由一串字符(`char`)组成的,并以一个空终结字符结尾。字符串是 C 语言的一个重要且复杂的部分,我们会在接下来的几章中详细的学习它。 100 | 101 | 字符串还可以字面量来表示,将要表示的字符串包裹在双引号 `"` 中就可以了。比如说我们之前用过的 `"Hello, world!"` 。现在,你只需要记住,只要看到 `char*` 类型,就当成字符串来看待就可以了。 102 | 103 | ## 条件分支 104 | 105 | 条件分支语句可以让程序在只有条件为真的时候才去执行一段代码。 106 | 107 | 我们使用 `if` 关键字来声明条件语句,执行条件跟在后面,包裹在一对圆括号里,后面紧跟一对花括号里面包裹着条件为真时执行的代码。`if` 语句后面可以跟一个 `else` 语句,包含条件为假时执行的代码。 108 | 109 | 我们可以使用*或操作符*(`||`)和*并操作符*(`&&`)将多个条件组合在一起。 110 | 111 | 在条件语句中,凡是不为 0 的结果都是真的。这点需要牢记,因为很多条件测试语句都是以此为依据编写的。 112 | 113 | 检测 x 是否在 10 到 100 之间的条件语句可以这样写: 114 | 115 | ```c 116 | if (x > 10 && x < 100) { 117 | puts("x is greater than 10 and less than 100!"); 118 | } else { 119 | puts("x is less than 11 or greater than 99!"); 120 | } 121 | ``` 122 | 123 | ## 循环 124 | 125 | 循环可以不断的执行一段代码,直到条件变为假,或者计数完成。 126 | 127 | 在 C 语言中有两种类型的循环。第一种是 `while` 循环。`while` 循环不断地执行一段代码,直到条件为假停止。首先 `while` 关键字在前,后面紧跟包裹在一对圆括号中的条件语句,最后是包裹在一对大括号中的待执行语句。下面是一个例子: 128 | 129 | ```c 130 | int i = 10; 131 | while (i > 0) { 132 | puts("Loop Iteration"); 133 | i = i - 1; 134 | } 135 | ``` 136 | 137 | 第二种循环是 `for` 循环,跟 `while` 的条件语句不同的是,`for` 循环需要三个以 `;` 隔开的表达式:一个初始化语句,一个条件语句和一个递增语句。其中初始化语句在循环开始之前执行;条件语句每次迭代都会判断一次,如果为假,循环就退出了;递增语句在每次迭代的最后执行。`for` 循环通常用来计数,因为它的表示方法比 `while` 更加简洁。下面的例子中,我们从 0 递增到 9,每次加 1,共执行了 10 次: 138 | 139 | ```c 140 | for (int i = 0; i < 10; i++) { 141 | puts("Loop Iteration"); 142 | } 143 | ``` 144 | 145 | ## 彩蛋 146 | 147 | - 使用 `for` 循环打印 5 次 `"Hello, world!"`。 148 | - 使用 `while` 循环打印 5 次 `"Hello, world!"`。 149 | - 编写一个函数 `Hello`,可以打印 n 次 `"Hello world!"`,然后在 `main` 函数中调用它。 150 | - 除了**变量**一节中列出的内建类型,还有哪些呢? 151 | - 除了 `>` 和 `<`,还有哪些比较操作符? 152 | - 除了 `+` 和 `-`,还有哪些算数运算符? 153 | - `+=` 运算符是什么?它是如何工作的? 154 | - `do...while` 循环是是什么?它是如何工作的? 155 | - `switch` 语句是什么?它是如何工作的? 156 | - `break` 关键字是用来干嘛的? 157 | - `continue` 关键字是用来干嘛的? 158 | - `typedef` 关键字的作用是什么? 159 | -------------------------------------------------------------------------------- /Bonus Projects.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksco/BuildYourOwnLispCn/b3d3ebd3187a85909d137a7a4d88612912cd4c02/Bonus Projects.md -------------------------------------------------------------------------------- /Conditionals.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksco/BuildYourOwnLispCn/b3d3ebd3187a85909d137a7a4d88612912cd4c02/Conditionals.md -------------------------------------------------------------------------------- /Error Handling.md: -------------------------------------------------------------------------------- 1 | # 第零八章 • 错误处理 2 | 3 | ## 异常退出 4 | 5 | 你可能已经注意到,上一章中写就的程序是存在问题的。试着输入下面的语句,看看会发生什么。 6 | 7 | ``` 8 | Lispy Version 0.0.0.0.3 9 | Press Ctrl+c to Exit 10 | 11 | lispy> / 10 0 12 | ``` 13 | 噢!程序竟然崩溃了,因为 0 不能作为除数。在开发过程中,程序崩溃是很正常的。但我们希望最后发布的产品能够告诉用户错误出在哪里,而不是粗暴的崩溃。 14 | 15 | 目前,我们的程序仅能打印出语法上的错误,但对于表达式求值过程中产生的错误却无能为力。C 语言并不擅长错误处理,但这却是不可避免的。而且随着系统复杂度的提升,后期再开始做的话难度就更大了。 16 | 17 | C 程序的“崩溃传统”历史悠久。任何程序出了错,操作系统只管将其内存回收。程序崩溃的原因和方式也千奇百怪。但是 C 程序并非是由魔法驱动的,如果你的程序运行出了错,与其坐在屏幕前“望眼欲穿”,不如借此机会学习一下调试工具 `gdb` 和 `valgrind` 的用法。学会使用这些强大的工具,会让你事半功倍。 18 | 19 | ## Lisp Value 20 | 21 | C 语言有很多种错误处理方式,但针对当前的项目,我更加倾向于使错误也成为表达式求值的结果。也就是说,在 Lispy 中,表达式求值的结果要么是*数字*,要么便是*错误*。举例说,表达式 `+ 1 2` 求值会得到数字 `3`,而表达式 `/ 10 0` 求值则会得到一个错误。 22 | 23 | 为了达到这个目的,我们需要能表示这两种结果的数据结构。简单起见,我们使用结构体来表示,并使用 `type` 字段来告诉我们当前哪个字段是有意义的。 24 | 25 | 结构体名为 `lval`,取义 *Lisp Value*,定义如下: 26 | 27 | ```c 28 | /* Declare New lval Struct */ 29 | typedef struct { 30 | int type; 31 | long num; 32 | int err; 33 | } lval; 34 | ``` 35 | 36 | ## 枚举 37 | 38 | 你或许已经注意到了,`lval` 的 `type` 和 `err` 字段的类型都是 `int`,这意味着它们皆由整数值来表示。 39 | 40 | 之所以选用 `int`,是因为我们将为每个整数值赋予意义,并在需要的时候进行解读。举例来说,我们可以制定这样的规则: 41 | 42 | - 如果 `type` 为 0,那么此结构体表示一个*数字*。 43 | - 如果 `type` 为 1,那么此结构体表示一个*错误*。 44 | 45 | 这是个简单而高效的方法。 46 | 47 | 但如果我们的代码中充斥了类似于 0 和 1 之类的“魔法数字”(Magic Number),程序的可读性就会大大降低。如果我们给这些数字起一个有意义的名字,就会给代码阅读者一些有用的提示,提高可读性。 48 | 49 | C 语言为此提供了语言特性上的支持——枚举(`enum`)。 50 | 51 | ```c 52 | /* Create Enumeration of Possible lval Types */ 53 | enum { LVAL_NUM, LVAL_ERR }; 54 | ``` 55 | 56 | `enum` 语句声明了一系列整型常量,并自动为它们赋值(译者注:从 0 开始,依次递增)。上面的代码展示了如何为 `type` 字段声明枚举值。 57 | 58 | 另外,我们还需要为 `error` 字段也声明一些枚举值。目前,我们需要声明三种类型的错误,包括:除数为零、操作符未知、操作数过大。代码如下: 59 | 60 | ```c 61 | /* Create Enumeration of Possible Error Types */ 62 | enum { LERR_DIV_ZERO, LERR_BAD_OP, LERR_BAD_NUM }; 63 | ``` 64 | 65 | ## Lisp Value 函数 66 | 67 | 我们的 `lval` 类型已经跃跃欲试了,但我们没有方法能创建新的实例。所以我们定义了两个函数来完成这项任务: 68 | 69 | ```c 70 | /* Create a new number type lval */ 71 | lval lval_num(long x) { 72 | lval v; 73 | v.type = LVAL_NUM; 74 | v.num = x; 75 | return v; 76 | } 77 | 78 | /* Create a new error type lval */ 79 | lval lval_err(int x) { 80 | lval v; 81 | v.type = LVAL_ERR; 82 | v.err = x; 83 | return v; 84 | } 85 | ``` 86 | 87 | 因为 `lval` 是一个结构体,所以已经不能简单的使用 `printf` 函数打印它了。对于不同类型的 `lval` 我们应该都能正确地打印出来。C 语言为此种需求提供了方便快捷的 `switch` 语句。它把输入和每种情况(`case`)相比较,如果值相等,它就会执行其中的代码,直到遇到 `break` 语句为止。 88 | 89 | 利用 `switch`,我们就可以轻松完成需求了: 90 | 91 | ```c 92 | /* Print an "lval" */ 93 | void lval_print(lval v) { 94 | switch (v.type) { 95 | /* In the case the type is a number print it */ 96 | /* Then 'break' out of the switch. */ 97 | case LVAL_NUM: printf("%li", v.num); break; 98 | 99 | /* In the case the type is an error */ 100 | case LVAL_ERR: 101 | /* Check what type of error it is and print it */ 102 | if (v.err == LERR_DIV_ZERO) { 103 | printf("Error: Division By Zero!"); 104 | } 105 | if (v.err == LERR_BAD_OP) { 106 | printf("Error: Invalid Operator!"); 107 | } 108 | if (v.err == LERR_BAD_NUM) { 109 | printf("Error: Invalid Number!"); 110 | } 111 | break; 112 | } 113 | } 114 | 115 | /* Print an "lval" followed by a newline */ 116 | void lval_println(lval v) { lval_print(v); putchar('\n'); } 117 | ``` 118 | 119 | ## 求值 120 | 121 | 现在知道了 `lval` 类型的使用方法,我们需要用它来替换掉之前使用的 `long` 类型。 122 | 123 | 这不仅仅是简单地将 `long` 替换为 `lval`,我们还需要修改函数使其能正确处理*数字*或是*错误*作为输入的情况。 124 | 125 | 在 `eval_op` 函数中,如果检测到错误,函数应该立即返回,当且仅当两个操作数都为数字类型时才做计算。另外,对于本章开头的除数为零的错误,也应该返回错误信息。 126 | 127 | ```c 128 | lval eval_op(lval x, char* op, lval y) { 129 | 130 | /* If either value is an error return it */ 131 | if (x.type == LVAL_ERR) { return x; } 132 | if (y.type == LVAL_ERR) { return y; } 133 | 134 | /* Otherwise do maths on the number values */ 135 | if (strcmp(op, "+") == 0) { return lval_num(x.num + y.num); } 136 | if (strcmp(op, "-") == 0) { return lval_num(x.num - y.num); } 137 | if (strcmp(op, "*") == 0) { return lval_num(x.num * y.num); } 138 | if (strcmp(op, "/") == 0) { 139 | /* If second operand is zero return error */ 140 | return y.num == 0 141 | ? lval_err(LERR_DIV_ZERO) 142 | : lval_num(x.num / y.num); 143 | } 144 | 145 | return lval_err(LERR_BAD_OP); 146 | } 147 | ``` 148 | 149 | 另外,`eval` 函数也需要小小地修整一下,为数字转换部分增加一点错误处理代码。 150 | 151 | 新代码中,我们选用 `strtol` 函数进行字符串到数字的转换,因为可以通过检测 `errno` 变量确定是否转换成功。这无疑比使用 `atoi` 函数更为明智。 152 | 153 | ```c 154 | lval eval(mpc_ast_t* t) { 155 | 156 | if (strstr(t->tag, "number")) { 157 | /* Check if there is some error in conversion */ 158 | errno = 0; 159 | long x = strtol(t->contents, NULL, 10); 160 | return errno != ERANGE ? lval_num(x) : lval_err(LERR_BAD_NUM); 161 | } 162 | 163 | char* op = t->children[1]->contents; 164 | lval x = eval(t->children[2]); 165 | 166 | int i = 3; 167 | while (strstr(t->children[i]->tag, "expr")) { 168 | x = eval_op(x, op, eval(t->children[i])); 169 | i++; 170 | } 171 | 172 | return x; 173 | } 174 | ``` 175 | 176 | 最后的一小步!使用新定义的打印函数: 177 | 178 | ```c 179 | lval result = eval(r.output); 180 | lval_println(result); 181 | mpc_ast_delete(r.output); 182 | ``` 183 | 184 | 完成!尝试运行新程序,确保除数为零时不会崩溃了:) 185 | 186 | ``` 187 | lispy> / 10 0 188 | Error: Division By Zero! 189 | lispy> / 10 2 190 | 5 191 | ``` 192 | 193 | ## 参考 194 | 195 | **error_handling.c** 196 | 197 | ```c 198 | #include "mpc.h" 199 | 200 | #ifdef _WIN32 201 | 202 | static char buffer[2048]; 203 | 204 | char* readline(char* prompt) { 205 | fputs(prompt, stdout); 206 | fgets(buffer, 2048, stdin); 207 | char* cpy = malloc(strlen(buffer)+1); 208 | strcpy(cpy, buffer); 209 | cpy[strlen(cpy)-1] = '\0'; 210 | return cpy; 211 | } 212 | 213 | void add_history(char* unused) {} 214 | 215 | #else 216 | #include 217 | #include 218 | #endif 219 | 220 | /* Create Enumeration of Possible Error Types */ 221 | enum { LERR_DIV_ZERO, LERR_BAD_OP, LERR_BAD_NUM }; 222 | 223 | /* Create Enumeration of Possible lval Types */ 224 | enum { LVAL_NUM, LVAL_ERR }; 225 | 226 | /* Declare New lval Struct */ 227 | typedef struct { 228 | int type; 229 | long num; 230 | int err; 231 | } lval; 232 | 233 | /* Create a new number type lval */ 234 | lval lval_num(long x) { 235 | lval v; 236 | v.type = LVAL_NUM; 237 | v.num = x; 238 | return v; 239 | } 240 | 241 | /* Create a new error type lval */ 242 | lval lval_err(int x) { 243 | lval v; 244 | v.type = LVAL_ERR; 245 | v.err = x; 246 | return v; 247 | } 248 | 249 | /* Print an "lval" */ 250 | void lval_print(lval v) { 251 | switch (v.type) { 252 | /* In the case the type is a number print it */ 253 | /* Then 'break' out of the switch. */ 254 | case LVAL_NUM: printf("%li", v.num); break; 255 | 256 | /* In the case the type is an error */ 257 | case LVAL_ERR: 258 | /* Check what type of error it is and print it */ 259 | if (v.err == LERR_DIV_ZERO) { 260 | printf("Error: Division By Zero!"); 261 | } 262 | if (v.err == LERR_BAD_OP) { 263 | printf("Error: Invalid Operator!"); 264 | } 265 | if (v.err == LERR_BAD_NUM) { 266 | printf("Error: Invalid Number!"); 267 | } 268 | break; 269 | } 270 | } 271 | 272 | /* Print an "lval" followed by a newline */ 273 | void lval_println(lval v) { lval_print(v); putchar('\n'); } 274 | 275 | lval eval_op(lval x, char* op, lval y) { 276 | 277 | /* If either value is an error return it */ 278 | if (x.type == LVAL_ERR) { return x; } 279 | if (y.type == LVAL_ERR) { return y; } 280 | 281 | /* Otherwise do maths on the number values */ 282 | if (strcmp(op, "+") == 0) { return lval_num(x.num + y.num); } 283 | if (strcmp(op, "-") == 0) { return lval_num(x.num - y.num); } 284 | if (strcmp(op, "*") == 0) { return lval_num(x.num * y.num); } 285 | if (strcmp(op, "/") == 0) { 286 | /* If second operand is zero return error */ 287 | return y.num == 0 288 | ? lval_err(LERR_DIV_ZERO) 289 | : lval_num(x.num / y.num); 290 | } 291 | 292 | return lval_err(LERR_BAD_OP); 293 | } 294 | 295 | lval eval(mpc_ast_t* t) { 296 | 297 | if (strstr(t->tag, "number")) { 298 | /* Check if there is some error in conversion */ 299 | errno = 0; 300 | long x = strtol(t->contents, NULL, 10); 301 | return errno != ERANGE ? lval_num(x) : lval_err(LERR_BAD_NUM); 302 | } 303 | 304 | char* op = t->children[1]->contents; 305 | lval x = eval(t->children[2]); 306 | 307 | int i = 3; 308 | while (strstr(t->children[i]->tag, "expr")) { 309 | x = eval_op(x, op, eval(t->children[i])); 310 | i++; 311 | } 312 | 313 | return x; 314 | } 315 | 316 | int main(int argc, char** argv) { 317 | 318 | mpc_parser_t* Number = mpc_new("number"); 319 | mpc_parser_t* Operator = mpc_new("operator"); 320 | mpc_parser_t* Expr = mpc_new("expr"); 321 | mpc_parser_t* Lispy = mpc_new("lispy"); 322 | 323 | mpca_lang(MPCA_LANG_DEFAULT, 324 | " \ 325 | number : /-?[0-9]+/ ; \ 326 | operator : '+' | '-' | '*' | '/' ; \ 327 | expr : | '(' + ')' ; \ 328 | lispy : /^/ + /$/ ; \ 329 | ", 330 | Number, Operator, Expr, Lispy); 331 | 332 | puts("Lispy Version 0.0.0.0.4"); 333 | puts("Press Ctrl+c to Exit\n"); 334 | 335 | while (1) { 336 | 337 | char* input = readline("lispy> "); 338 | add_history(input); 339 | 340 | mpc_result_t r; 341 | if (mpc_parse("", input, Lispy, &r)) { 342 | lval result = eval(r.output); 343 | lval_println(result); 344 | mpc_ast_delete(r.output); 345 | } else { 346 | mpc_err_print(r.error); 347 | mpc_err_delete(r.error); 348 | } 349 | 350 | free(input); 351 | 352 | } 353 | 354 | mpc_cleanup(4, Number, Operator, Expr, Lispy); 355 | 356 | return 0; 357 | } 358 | ``` 359 | 360 | ## 彩蛋 361 | 362 | - 怎样枚举(`enum`)制定一个名字? 363 | - 什么是联合(`union`),它是怎么工作的? 364 | - 相比于结构体,联合的优势在哪? 365 | - 你能用在 `lval` 的定义中使用 `union` 吗? 366 | - 扩展分析和求值部分,使其支持求膜操作符(`%`) 367 | - 扩展分析和求值部分,使其支持浮点数(`double`) -------------------------------------------------------------------------------- /Evaluation.md: -------------------------------------------------------------------------------- 1 | # 第零七章 • 计算 2 | 3 | ## 树型结构 4 | 5 | 现在,我们可以读取输入,解析并得到表达式的内部结构。但是并不能对它进行计算。本章我们将编写代码,对表达式的内部结构进行计算求值。 6 | 7 | 所谓的内部结构就是上一章中打印出来的内容。它被称为*抽象语法树*(Abstract Syntax Tree,简称 AST)。它用来表示用户输入的表达式的结构。操作数和操作符等需要被处理的实际数据都位于叶子节点上。而非叶子节点上则包含了遍历和求值的信息。 8 | 9 | 在做解析和求值之前,先来看一下数据结构具体的内部表示。在 `mpc.h` 中,可以找到 `mpc_ast_t` 类型的定义,这里面就是解析表达式得到的数据结构。 10 | 11 | ```c 12 | typedef struct mpc_ast_t { 13 | char* tag; 14 | char* contents; 15 | mpc_state_t state; 16 | int children_num; 17 | struct mpc_ast_t** children; 18 | } mpc_ast_t; 19 | ``` 20 | 21 | 下面来逐一的看看结构体各个字段的含义。 22 | 23 | 第一个为 `tag` 字段。在打印这个树形结构时,`tag` 就是在节点内容之前的信息,它表示了解析这个节点时所用到的所有规则。例如:`expr|number|regex`。 24 | 25 | `tag` 字段非常重要,因为它可以让我们知道创建节点时所匹配到的规则。 26 | 27 | 第二个是 `contents` 字段,它包含了节点中具体的内容,例如 `'*'`、`'('`、`'5'`。你会发现,对于表示分支的非叶子节点,这个字段为空。而对于叶子节点,则包含了操作数或操作符的字符串形式。 28 | 29 | 下一个字段是 `state`。这里面包含了解析器发现这个节点时所处的状态,例如行数和列数等信息。本书不会用到这个字段。 30 | 31 | 最后的两个字段 `children_num` 和 `children` 帮助我们来遍历抽象语法树。前一个字段告诉我们有多少个孩子节点,后一个字段是包含这些节点的数组。 32 | 33 | 其中,`children` 字段的类型是 `mpc_ast_t**`。这是一个二重指针类型。实际上,它并不像看起来那么可怕,我会在后面的章节中详细解释它。现在你只需要知道它是孩子节点的列表即可。 34 | 35 | 我们可以对 `children` 使用数组的语法,在其后使用 `[x]` 来获取某个下标的值。比如,可以用 `children[0]` 来获取第一个孩子节点。注意,在 C 语言中数组是从 `0` 开始计数的。 36 | 37 | 因为 `mpc_ast_t*` 是指向结构体的指针类型,所以获取其字段的语法有些许不同。我们需要使用 `->` 符号,而不是 `.` 符号。 38 | 39 | ```c 40 | /* Load AST from output */ 41 | mpc_ast_t* a = r.output; 42 | printf("Tag: %s\n", a->tag); 43 | printf("Contents: %s\n", a->contents); 44 | printf("Number of children: %i\n", a->children_num); 45 | 46 | /* Get First Child */ 47 | mpc_ast_t* c0 = a->children[0]; 48 | printf("First Child Tag: %s\n", c0->tag); 49 | printf("First Child Contents: %s\n", c0->contents); 50 | printf("First Child Number of children: %i\n", 51 | c0->children_num); 52 | ``` 53 | 54 | ## 递归(Recursion) 55 | 56 | 树形结构是自身重复的。树的每个孩子节点都是树,每个孩子节点的孩子节点也是树,以此类推。正如编程语言一样,树形结构也是递归和重复的。显然,如果我们想编写函数处理所有可能的情况,就必须要保证函数可以处理任意深度。幸运的是,我们可以使用递归函数的天生优势来轻松地处理这种重复自身的结构。 57 | 58 | 简而言之,递归函数就是在执行的过程中调用自身的函数。这听起来可能有些奇怪,因为这会导致函数无穷尽地执行下去。但是函数对于不同的输入会产生不同的输出,如果我们每次递归都改变或使用不同的输入,并设置递归终止的条件,我们就可以使用递归做一些有用的事情。 59 | 60 | 举个例子,我们可以使用递归来计算树形结构中节点个数。 61 | 62 | 首先考虑最简单的情况,如果输入的树没有子节点,我们只需简单的返回 `1` 就行了。如果输入的树有一个或多个子节点,这时返回的结果就是自身节点的 `1`,加上所有子节点的值。 63 | 64 | 但我们怎样得到所有子节点的值呢?这正是我们要着手解决的问题。请看下面的 C 语言代码。 65 | 66 | ```c 67 | int number_of_nodes(mpc_ast_t* t) { 68 | if (t->children_num == 0) { return 1; } 69 | if (t->children_num >= 1) { 70 | int total = 1; 71 | for (int i = 0; i < t->children_num; i++) { 72 | total = total + number_of_nodes(t->children[i]); 73 | } 74 | return total; 75 | } 76 | } 77 | ``` 78 | 79 | 递归函数的定义可能看起来有些奇怪。我们首先假定存在某个函数能够正确的工作,然后使用它来编写刚刚假定存在的函数。 80 | 81 | 正如世间万物,递归函数也有规律可循。首先需要定义的是最基本的情况,用来终止递归的执行。例如上例中的 `t->children_num == 0`。之后定义的是需要递归的情况,例如上例中的 `t->children_num >= 1`。这部分将计算过程分为几个相似的部分,然后调用自身来递归处理这些部分,最后将结果整合起来。 82 | 83 | 理解递归函数需要动一番脑筋,在继续之前,请确保自己理解了上面的内容。在后面的章节中,我们会大量地用到递归。如果你还是不清楚递归的原理,请参考本章福利部分的某些问题。 84 | 85 | ## 求值 86 | 87 | 为了解析求值前面生成的语法树,我们需要写一个递归函数。但是在开始之前,我们先观察一下树的结构。使用上一章的程序打印一些表达式的解析结果,你能观察到什么? 88 | 89 | ``` 90 | lispy> * 10 (+ 1 51) 91 | > 92 | regex 93 | operator|char:1:1 '*' 94 | expr|number|regex:1:3 '10' 95 | expr|> 96 | char:1:6 '(' 97 | operator|char:1:7 '+' 98 | expr|number|regex:1:9 '1' 99 | expr|number|regex:1:11 '51' 100 | char:1:13 ')' 101 | regex 102 | ``` 103 | 104 | 首先我们注意到,有 `number` 标签的节点一定是一个数字,并且没有孩子节点。我们可以直接将其转换为一个数字。这将是递归函数中的基本情况。 105 | 106 | 如果一个节点有 `expr` 标签,但没有 `number` 标签,我们需要看他的第二个孩子节点是什么操作符(第一个孩子节点永远是 `(` 字符)。然后我们需要使用这个操作符来对后面的孩子节点进行求值。当然,也不包括最后的 `)` 节点。这就是所谓的递归的情况啦。 107 | 108 | 在对语法树进行求值的时候,正如前面编写的 `number_of_nodes` 函数,需要保存计算的结果。在这里,我们使用 C 语言中 `long` 类型(长整形)。 109 | 110 | 另外,为了检测节点的类型,或是获得节点中保存的数值,我们会用到节点中的 `tag` 和 `contents` 字段。这些字段都是字符串类型的,所以需要用到一些辅助性的库函数: 111 | 112 | | 函数名 | 作用 | 113 | |-----------------|------------------------| 114 | | `atoi` | 将 `char*` 转化为 `long` 型 | 115 | | `strcmp` | 接受两个 `char*` 参数,比较他们是否相等,如果相等就返回 0 | 116 | | `strstr` | 接受两个 `char*`,如果第一个字符串包含第二个,返回其在第一个中首次出现的位置的指针,否则返回 0 | 117 | 118 | 我们可以使用 `strcmp` 来检查应该使用什么操作符,并使用 `strstr` 来检测 `tag` 中是否含有某个字段。有了这些基础,我们的递归求值函数就可以写出来啦: 119 | 120 | ```c 121 | long eval(mpc_ast_t* t) { 122 | 123 | /* If tagged as number return it directly. */ 124 | if (strstr(t->tag, "number")) { 125 | return atoi(t->contents); 126 | } 127 | 128 | /* The operator is always second child. */ 129 | char* op = t->children[1]->contents; 130 | 131 | /* We store the third child in `x` */ 132 | long x = eval(t->children[2]); 133 | 134 | /* Iterate the remaining children and combining. */ 135 | int i = 3; 136 | while (strstr(t->children[i]->tag, "expr")) { 137 | x = eval_op(x, op, eval(t->children[i])); 138 | i++; 139 | } 140 | 141 | return x; 142 | } 143 | ``` 144 | 145 | 其中,`eval_op` 函数的定义如下。它接受一个数字,一个操作符,和另一个数字。它检测操作符的类型,对其进行相应的计算,并将结果返回。 146 | 147 | ```c 148 | /* Use operator string to see which operation to perform */ 149 | long eval_op(long x, char* op, long y) { 150 | if (strcmp(op, "+") == 0) { return x + y; } 151 | if (strcmp(op, "-") == 0) { return x - y; } 152 | if (strcmp(op, "*") == 0) { return x * y; } 153 | if (strcmp(op, "/") == 0) { return x / y; } 154 | return 0; 155 | } 156 | ``` 157 | 158 | ##打印 159 | 160 | 有了求值函数,就不能满足于打印语法树了,现在我们可以打印语法树求值后的结果啦。 161 | 162 | ```c 163 | long result = eval(r.output); 164 | printf("%li\n", result); 165 | mpc_ast_delete(r.output); 166 | ``` 167 | 168 | 所有的工作完成无误后,就能看到我们的新语言执行一些基本的数学运算啦! 169 | 170 | ``` 171 | Lispy Version 0.0.0.0.3 172 | Press Ctrl+c to Exit 173 | 174 | lispy> + 5 6 175 | 11 176 | lispy> - (* 10 10) (+ 1 1 1) 177 | 97 178 | ``` 179 | 180 | ## 参考 181 | 182 | **evaluation.c** 183 | 184 | ```c 185 | #include "mpc.h" 186 | 187 | #ifdef _WIN32 188 | 189 | static char buffer[2048]; 190 | 191 | char* readline(char* prompt) { 192 | fputs(prompt, stdout); 193 | fgets(buffer, 2048, stdin); 194 | char* cpy = malloc(strlen(buffer)+1); 195 | strcpy(cpy, buffer); 196 | cpy[strlen(cpy)-1] = '\0'; 197 | return cpy; 198 | } 199 | 200 | void add_history(char* unused) {} 201 | 202 | #else 203 | #include 204 | #include 205 | #endif 206 | 207 | /* Use operator string to see which operation to perform */ 208 | long eval_op(long x, char* op, long y) { 209 | if (strcmp(op, "+") == 0) { return x + y; } 210 | if (strcmp(op, "-") == 0) { return x - y; } 211 | if (strcmp(op, "*") == 0) { return x * y; } 212 | if (strcmp(op, "/") == 0) { return x / y; } 213 | return 0; 214 | } 215 | 216 | long eval(mpc_ast_t* t) { 217 | 218 | /* If tagged as number return it directly. */ 219 | if (strstr(t->tag, "number")) { 220 | return atoi(t->contents); 221 | } 222 | 223 | /* The operator is always second child. */ 224 | char* op = t->children[1]->contents; 225 | 226 | /* We store the third child in `x` */ 227 | long x = eval(t->children[2]); 228 | 229 | /* Iterate the remaining children and combining. */ 230 | int i = 3; 231 | while (strstr(t->children[i]->tag, "expr")) { 232 | x = eval_op(x, op, eval(t->children[i])); 233 | i++; 234 | } 235 | 236 | return x; 237 | } 238 | 239 | int main(int argc, char** argv) { 240 | 241 | mpc_parser_t* Number = mpc_new("number"); 242 | mpc_parser_t* Operator = mpc_new("operator"); 243 | mpc_parser_t* Expr = mpc_new("expr"); 244 | mpc_parser_t* Lispy = mpc_new("lispy"); 245 | 246 | mpca_lang(MPCA_LANG_DEFAULT, 247 | " \ 248 | number : /-?[0-9]+/ ; \ 249 | operator : '+' | '-' | '*' | '/' ; \ 250 | expr : | '(' + ')' ; \ 251 | lispy : /^/ + /$/ ; \ 252 | ", 253 | Number, Operator, Expr, Lispy); 254 | 255 | puts("Lispy Version 0.0.0.0.3"); 256 | puts("Press Ctrl+c to Exit\n"); 257 | 258 | while (1) { 259 | 260 | char* input = readline("lispy> "); 261 | add_history(input); 262 | 263 | mpc_result_t r; 264 | if (mpc_parse("", input, Lispy, &r)) { 265 | 266 | long result = eval(r.output); 267 | printf("%li\n", result); 268 | mpc_ast_delete(r.output); 269 | 270 | } else { 271 | mpc_err_print(r.error); 272 | mpc_err_delete(r.error); 273 | } 274 | 275 | free(input); 276 | 277 | } 278 | 279 | mpc_cleanup(4, Number, Operator, Expr, Lispy); 280 | 281 | return 0; 282 | } 283 | ``` 284 | 285 | ## 彩蛋 286 | 287 | - 编写递归函数计算语法树的叶子节点个数。 288 | - 编写递归函数计算语法树中的分支个数。 289 | - 怎样使用 `strstr` 检查节点是否有 `expr` 标签(`tag`) 290 | - 怎样使用 `strcmp` 检查节点是否为 `(` 或 `)` 节点? 291 | - 添加 `%` 运算符,用于取余运算,如:`% 10 6` 等于 4。 292 | - 添加 `^` 运算符,用于指数运算,如:`^ 4 2` 等于 16。 293 | - 添加 `min` 函数,返回操作数中最小的数,如:`min 1 5 3` 等于 1。 294 | - 添加 `max` 函数,返回操作数中最大的数,如:`max 1 5 3` 等于 5。 295 | - 改变 `-` 字符的作用,使其后面只有一个操作数的时候,返回该数的负数。 296 | -------------------------------------------------------------------------------- /Functions.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksco/BuildYourOwnLispCn/b3d3ebd3187a85909d137a7a4d88612912cd4c02/Functions.md -------------------------------------------------------------------------------- /Installation.md: -------------------------------------------------------------------------------- 1 | # 第零二章 • 安装 2 | 3 | 在开始学习 C 语言之前,我们需要安装一些必要的东西,搭建好编程的环境。好在过程并不复杂,我们只需要两个工具:代码编辑器和编译器。 4 | 5 | ## 代码编辑器 6 | 7 | 代码编辑器其实就是一个更适合写代码的文本编辑器。它提供一些诸如关键字高亮,智能提示等功能帮助我们更快更好的编写代码。 8 | 9 | - 在 *Linux* 上,我建议你用 *gedit*。不过如果你手头上有现成的代码编辑器也是可以的,但请不要使用 **IDE**。杀鸡焉用宰牛刀,本书构建的小小程序不会用到 IDE 所提供的便利,反而让你不清楚发生了什么。 10 | - 在 *Mac OS X* 上,一个可以使用的编辑器是 [*TextWrangler*](http://www.barebones.com/products/textwrangler/),如果你有其他喜欢的,也是可以的,但请不要使用 **Xcode**,这种小项目使用 IDE 反而会让你搞不清楚细节。 11 | - 在 *Microsoft Windows* 上,我建议你使用 *Notepad++*,如果你有其他喜欢的也是可以的啦。但是请不要使用 **Visual Studio**,因为它对 C 语言的支持并不好,使用它会遇到很多问题。 12 | 13 | ## 编译器 14 | 15 | 编译器的作用是将我们写好的 C 语言的代码翻译成电脑能够直接运行的程序。不同的操作系统安装编译器的过程也是有差别的。 16 | 17 | 另外编译和运行 C 程序需要知道一些基本的命令行操作,本书不会教你怎么使用命令行。如果你从来没听说过命令行,你可以到网上搜一些教程看看。 18 | 19 | - 在 *Linux* 上,你可以通过下载并安装开发包获得 C 语言的编译器。如果你的系统是 Ubuntu 或 Debian,你可以通过这行命令来安装:`sudo apt-get install build-essential`。 20 | - 在 *Mac OS X* 上,你需要在应用商店里下载并安装最新版的 Xcode。然后在命令行中运行 `xcode-select --install` 来安装 *Command Line Tools*。 21 | - 在 *Microsoft Windows* 上,你可以下载并安装 [MinGW](http://www.mingw.org/),具体的安装及配置方法可以到网上搜几个教程看一看,这里不再细说。 22 | 23 | ## 测试安装好的 C 编译器 24 | 25 | 为了验证一下 C 编译器是否安装成功了,请在命令行中键入下面的语句并运行: 26 | 27 | `cc --version` 28 | 29 | 如果你得到了一些关于编译器版本的信息,那就说明安装成功了!譬如,在我的 Mac 上,返回信息如下所示: 30 | 31 | ``` 32 | $ cc --version 33 | Apple LLVM version 7.0.0 (clang-700.1.76) 34 | Target: x86_64-apple-darwin15.0.0 35 | Thread model: posix 36 | ``` 37 | 38 | ## Hello World 39 | 40 | 至此,环境搭建工作已经完成了。现在打开你的代码编辑器,将下方的代码输入到其中。新建一个文件夹,用于存放编写的代码。并把刚刚的代码保存到这个文件夹中,起名为 `hello_world.c`。这就是我们的第一个 C 程序啦! 41 | 42 | ```c 43 | #include 44 | 45 | int main(int argc, char** argv) { 46 | puts("Hello, world!"); 47 | return 0; 48 | } 49 | ``` 50 | 51 | 接下来我会一行一行的解释这个程序。 52 | 53 | 第一行,我们包含了一个名为 `stdio.h` 的头文件进来。这条语句让我们可以使用标准输入输出库所提供的函数。 54 | 55 | 接下来,我们声明一个名为 `main` 的函数,该函数接受一个 `int` 类型的输入 `argc`,和一个 `char**` 类型的输入 `argv`,返回 `int` 类型的值。所有的 C 程序都必须包含 `main` 函数,它是程序执行的起点。 56 | 57 | 在 `main` 函数当中调用了一个名为 `puts` 的函数(这个函数就是由 stdio.h 提供给我们的哦),并传递了 "Hello, world!" 参数,这个函数会将传进去的 Hello, world! 输出到命令行中。`puts` 是 put string 的简写形式。函数中的第二条语句是 `return 0;`。它的作用是结束 `main` 函数并返回 0 值。0 代表程序正常退出,没有发生错误。 58 | 59 | ## 编译 60 | 61 | 在运行这个程序之前,我们首先要将它编译成可执行的程序。打开命令行,然后跳转到 `hello_world.c` 被保存的目录。你可以通过运行以下命令来编译你的程序: 62 | 63 | `cc -std=c99 -Wall hello_world.c -o hello_world` 64 | 65 | 这条语句编译了 `hello_world.c` 里面的代码,产生了一个新的可执行文件,叫做 `hello_world`。`-std=c99` 是为了告诉编译器我们使用的是哪个标准的 C 语言。通过指定标准,只要我们写的程序符合标准的规范,我们的程序就可以在多个平台上编译并运行。 66 | 67 | 如果编译成功了,你会在 `hello_world.c` 同目录下看到一个名为 `hello_world` 的新文件。在命令行里面敲入 `./hello_world` ,按一下回车键,就可以运行这个程序了。会在命令行打印出 `Hello, world!` 字样。 68 | 69 | **恭喜!**你成功的编译并运行了你的第一个 C 程序! 70 | 71 | ## 错误 72 | 73 | 如果你的 C 语言程序写的存在问题,可能会导致编译失败。这个错误可能是简单的语法错误,也可能是其他的一些难以理解的复杂错误。 74 | 75 | 编译失败后,编译器会向你提供一些有用的错误信息,如果你看不懂,可以到网上搜索一下。你要相信,在你之前肯定有很多人遇到了和你一样的错误。另外,程序中可能同时存在多个错误,记得要从第一条开始解决。 76 | 77 | 而有时,程序虽然编译成功了,但是运行的时候却崩溃掉了。这就需要对程序进行一些调试性的工作,设法找出问题所在。调试程序是进阶内容,超出了本书的讨论范围。 78 | 79 | ## 文档 80 | 81 | 在本书提供的代码中,可能会遇到一些你从来没有见过的函数。如果想知道这个函数的作用,应该去查找 C 语言标准库的[在线文档](http://en.cppreference.com/w/c),这个文档中介绍了 C 语言中所有的标准库函数的作用及其用法。 82 | 83 | ## 参考 84 | 85 | > #### 参考是用来干嘛的? 86 | 87 | > 在参考部分,我通常会给出这章中用到的代码作为总览。如果你自己写的程序遇到了错误,请不要把这里的代码简单的拷贝粘贴就完事了。一定要自己尝试着找出错误,并解决它。 88 | 89 | `hello_world.c` 90 | 91 | ```c 92 | #include 93 | 94 | int main(int argc, char** argv) { 95 | puts("Hello, world!"); 96 | return 0; 97 | } 98 | ``` 99 | 100 | ## 彩蛋 101 | 102 | > #### 彩蛋是用来干嘛的? 103 | 104 | > 在彩蛋部分,我通常会提出一些有趣的,有挑战性的问题。请尽力尝试解决它们。你没有必要把它们全做出来,因为有些是很有难度的。 105 | 106 | - 将 `"Hello, world!"` 改为其他的问候语。 107 | - 如果没有 `main` 函数,编译会得到什么错误? 108 | - 使用[在线文档](http://en.cppreference.com/w/c)查看 `puts` 函数的用法。 109 | -------------------------------------------------------------------------------- /Interactive.md: -------------------------------------------------------------------------------- 1 | # 第零四章 • 交互 2 | 3 | ## 读取-求值-输出 4 | 5 | 在编写我们的 Lisp 之前,我们需要寻找一种和它交互的方式。最简单的方法,我们可以修改代码,重新编译,然后再次运行。这个方案虽然理论上可行,但是太为繁琐。如果可以动态地和程序进行交互,我们就可以快速地测试程序在各种条件下的行为。我们称这种模式为交互提示。 6 | 7 | 这种模式下的程序读取用户的输入,在程序内部进行处理,然后返回一些信息给用户。这种系统也被叫做 *REPL*,是 *read-evaluate-print loop* (读取-求值-输出循环) 的简写。这种技术被广泛地应用在各种编程语言的解释器中,如果你学过 *Python*,那你一定不会陌生。 8 | 9 | 在编写一个完整的 *REPL* 之前,我们先实现一个简单的程序:读取用户的输入,简单处理后返回给用户。在后面的章节中,我们会对这个程序不断扩展,最后能够正确地读取并解析一个真正的 Lisp 程序,并将结果返回给用户。 10 | 11 | ## 交互提示 12 | 13 | 为了实现这个简单的想法,可以使用一个循环不断打印信息并等待用户输入。为了获取用户输入的内容,我们可以使用 `stdio.h` 中的 `fgets` 函数。这个函数可以一直读取直到遇到换行符为止。我们需要找个地方存储用户的输入。为此可以声明一个固定大小的数组缓冲区。 14 | 15 | 一旦获取到用户输入的字符串,就可以使用 `printf` 将它打印到命令行中。 16 | 17 | ```c 18 | #include 19 | 20 | /* Declare a buffer for user input of size 2048 */ 21 | static char input[2048]; 22 | 23 | int main(int argc, char** argv) { 24 | 25 | /* Print Version and Exit Information */ 26 | puts("Lispy Version 0.0.0.0.1"); 27 | puts("Press Ctrl+c to Exit\n"); 28 | 29 | /* In a never ending loop */ 30 | while (1) { 31 | 32 | /* Output our prompt */ 33 | fputs("lispy> ", stdout); 34 | 35 | /* Read a line of user input of maximum size 2048 */ 36 | fgets(input, 2048, stdin); 37 | 38 | /* Echo input back to user */ 39 | printf("No you're a %s", input); 40 | } 41 | 42 | return 0; 43 | } 44 | 45 | ``` 46 | 47 | > #### 代码中的 `/*...*/` 是什么? 48 | 49 | > 这是 C 语言中的注释,是为了向其它阅读代码的人解释代码作用的。在编译的时候,会被编译器忽略掉。 50 | 51 | 现在来深入解读一下这个程序。 52 | 53 | `static char input[2048];` 这行代码声明了一个拥有 2048 个字符长度的全局数组。这个数组中存储的数据可以在程序的任何地方获取到。我们会把用户在命令中输入的语句保存到这里面来。`static` 关键字标明这个数组仅在本文件中可见。`[2048]` 表明了数组的大小。 54 | 55 | 我们使用 `while(1)` 来构造一个无限循环,条件语句 `1` 永远都为真,所以这个循环会一直执行下去。 56 | 57 | 我们使用 `fputs` 打印提示信息。这个函数和前面介绍过的 `puts` 函数区别是 `fputs` 不会在末尾自动加换行符。我们使用 `fgets` 函数来获取用户在命令行中输入的字符串。这两个函数都需要指定写入或读取的文件。在这里,我们使用 `stdin` 和 `stdout` 作为输入和输出。这两个变量都是在 `` 中定义的,用来表示向命令行进行输入和输出。当我们把 `stdin` 传给 `fgets` 后,它就会等待用户输入一串字符,并按下回车键。如果得到了字串,就会把字串连同换行符存放到 `input` 数组中。为了不让获取到的数据太大数组装不下,我们还要指定一下可以获取的最大长度为 `2048`。 58 | 59 | 我们使用 `printf` 函数将处理后的信息返回给用户。`printf` 允许我们同时打印多个不同类型的值。它会自动对第一个字符串参数中的模式进行匹配。例如,在上面的例子中,可以在第一个参数中看到 `%s` 字样。`printf` 将自动把 `%s` 替换为后面的参数中的值。`s` 代表字符串(`string`)。 60 | 61 | 更多关于 `printf` 的模式种类及其用法,可以参考[文档](http://en.cppreference.com/w/c/io/printf)。 62 | 63 | > #### 我怎么才能知道一些类似于 `fgets` 或 `printf` 的函数的用法? 64 | 65 | > 很明显你不可能一开始就知道这些标准库函数的作用和用法,这些都需要经验。幸运的是,C 语言的标准库非常精炼。绝大多数的库函数都可以在平时的练习中了解并学会使用。如果你想要解决的是底层的、基本的问题,关注一下[参考文档](http://en.cppreference.com/w/c)会大有裨益,因为很可能标准库中的某个函数所做的事情正是你想要的! 66 | 67 | ## 编译 68 | 69 | 你可以使用在第二章介绍过的命令来编译上面的程序: 70 | 71 | `cc -std=c99 -Wall prompt.c -o prompt` 72 | 73 | 编译通过之后,你应该试着运行并测试一下这个程序。测试完成后,可以使用 `Ctrl+c` 快捷键来退出程序。如果一切正常,你会得到类似于下面的结果: 74 | 75 | 76 | ``` 77 | Lispy Version 0.0.0.0.1 78 | Press Ctrl+c to Exit 79 | 80 | lispy> hello 81 | No You're a hello 82 | lispy> my name is Dan 83 | No You're a my name is Dan 84 | lispy> Stop being so rude! 85 | No You're a Stop being so rude! 86 | lispy> 87 | ``` 88 | 89 | ## 编辑输入 90 | 91 | 如果你用的是 Mac 或 Linux,当你用左右箭头键编辑在程序中的输入时,你会遇到一个奇怪的问题: 92 | 93 | ``` 94 | Lispy Version 0.0.0.0.3 95 | Press Ctrl+c to Exit 96 | 97 | lispy> hel^[[D^[[C 98 | ``` 99 | 100 | 使用箭头键不会前后移动输入的光标,而是会产生像 `^[[D` 或 `^[[C` 这种奇怪的字符。很明显这不是我们想要的结果。 101 | 102 | 而在 Windows 上则不会有这个现象。 103 | 104 | 在 Mac 和 Linux 上,我们需要用到 `editline` 库来解决这个问题。并把 `fputs` 和 `fgets` 替换为这个库提供的相同功能的函数。 105 | 106 | 如果你用的是 Windows 系统,则可以直接跳到本章的最后。因为接下来的几个小节都是和安装与配置 `editline` 相关的内容。 107 | 108 | ## 使用 editline 库 109 | 110 | 我们会用到 `editline` 库提供的两个函数:`readline` 和 `add_history`。`readline` 和 `fgets` 一样,从命令行读取一行输入,并且允许用户使用左右箭头进行编辑。`add_history` 可以纪录下我们之前输入过的命令,并使用上下箭头来获取。新的程序如下所示: 111 | 112 | ```c 113 | #include 114 | #include 115 | 116 | #include 117 | #include 118 | 119 | int main(int argc, char** argv) { 120 | 121 | /* Print Version and Exit Information */ 122 | puts("Lispy Version 0.0.0.0.1"); 123 | puts("Press Ctrl+c to Exit\n"); 124 | 125 | /* In a never ending loop */ 126 | while (1) { 127 | 128 | /* Output our prompt and get input */ 129 | char* input = readline("lispy> "); 130 | 131 | /* Add input to history */ 132 | add_history(input); 133 | 134 | /* Echo input back to user */ 135 | printf("No you're a %s\n", input); 136 | 137 | /* Free retrieved input */ 138 | free(input); 139 | 140 | } 141 | 142 | return 0; 143 | } 144 | ``` 145 | 146 | 我们增加了一些新的头文件。`` 提供了 `free` 函数。`` 和 `` 提供了 `editline` 库中的 `readline` 和 `add_history` 函数。 147 | 148 | 在上面的程序中,我们使用 `readline` 读取用户输入,使用 `add_history` 将该输入添加到历史纪录当中,最后使用 `printf` 将其加工并打印出来。 149 | 150 | 与 `fgets` 不同的是,`readline` 并不在结尾添加换行符。所以我们在 `printf` 函数中添加了一个换行符。另外,我们还需要使用 `free` 函数手动释放 `readline` 函数返回给我们的缓冲区 `input`。这是因为 `readline` 不同于 `fgets` 函数,后者使用已经存在的空间,而前者会申请一块新的内存,所以需要手动释放。内存的申请与释放问题我们会在后面的章节中深入讨论。 151 | 152 | ## 链接 editline 并编译 153 | 154 | 如果你使用前面我们提供的命令行来编译这个程序,你会得到类似于下面的错误,因为在使用之前,你必须先在电脑上安装 `editline` 库。 155 | 156 | `fatal error: editline/readline.h: No such file or directory #include ` 157 | 158 | 在 Mac 上,`editline` 包含在 *Command Line Tools* 中,安装方法我们在第二章有说明。安装完后,可能还是会出现头文件不存在的编译错误。这时,可以移除 `#include ` 这行代码,再试一次。 159 | 160 | 在 Linux 上,可以使用 `sudo apt-get install libedit-dev` 来安装 `editline`。在 Fedora 上,使用 `su -c "yum install libedit-dev*"` 命令安装。 161 | 162 | 一旦你安装好了 `editline`,你可以再次编译试一下。然后将会得到如下的错误: 163 | 164 | ``` 165 | undefined reference to `readline' 166 | undefined reference to `add_history' 167 | ``` 168 | 169 | 这是因为没有将 `editline` 链接到程序中。我们需要使用 `-ledit` 标记来完成链接,用法如下: 170 | 171 | `cc -std=c99 -Wall prompt.c -ledit -o prompt` 172 | 173 | 再次运行程序,就可以自由地编辑输入的文字了! 174 | 175 | > #### 为什么我的程序还是不能编译? 176 | 177 | >在有些系统上,`editline` 的安装、包含、链接的方式可能会有些许差别,请善用搜索引擎哦。 178 | 179 | ## 预处理器 180 | 181 | 对于这样的一个小程序而言,我们针对不同的系统编写不同的代码是可以的。但是如果我把我的代码发给一个使用不同的操作系统的朋友,让他帮我完善一下代码,可能就会出问题了。理想情况下,我希望我的代码可以在任何操作系统上编译并运行。这在 C 语言中是个很普遍的问题,叫做可移植性(portability)。这通常都是个很棘手的问题。 182 | 183 | 但是 C 提供了一个机制来帮助我们,叫做预处理器(preprocessor)。 184 | 185 | 预处理器也是一个程序,它在编译之前运行。它有很多作用。而我们之前就已经悄悄地用过预处理器了。任何以井号 `#` 开头的语句都是一个预处理命令。为了使用标准库中的函数,我们已经用它来包含(include)过头文件了。 186 | 187 | 预处理的另一个作用是检测当前的代码在哪个操作系统中运行,从而来产生平台相关的代码。而这也正是我们做可移植性工作时所需要的。 188 | 189 | 在 Windows 上,我们可以伪造一个 `readline` 和 `add_history` 函数,而在其他系统上就使用 `editline` 库提供给我们的真正有作用的函数。 190 | 191 | 为了达到这个目的,我们需要把平台相关的代码包在`#ifdef`、`#else` 和 `#endif` 预处理命令中。如果条件为真,包裹在 `#ifdef` 和 `#else` 之间的代码就会被执行,否则,`#else` 和 `endif` 之间的代码被执行。通过这个特性,我们就能写出在 Windows、Linux 和 Mac 三大平台上都能正确编译的代码了: 192 | 193 | ```c 194 | #include 195 | #include 196 | 197 | /* If we are compiling on Windows compile these functions */ 198 | #ifdef _WIN32 199 | #include 200 | 201 | static char buffer[2048]; 202 | 203 | /* Fake readline function */ 204 | char* readline(char* prompt) { 205 | fputs(prompt, stdout); 206 | fgets(buffer, 2048, stdin); 207 | char* cpy = malloc(strlen(buffer)+1); 208 | strcpy(cpy, buffer); 209 | cpy[strlen(cpy)-1] = '\0'; 210 | return cpy; 211 | } 212 | 213 | /* Fake add_history function */ 214 | void add_history(char* unused) {} 215 | 216 | /* Otherwise include the editline headers */ 217 | #else 218 | #include 219 | #include 220 | #endif 221 | 222 | int main(int argc, char** argv) { 223 | 224 | puts("Lispy Version 0.0.0.0.1"); 225 | puts("Press Ctrl+c to Exit\n"); 226 | 227 | while (1) { 228 | 229 | /* Now in either case readline will be correctly defined */ 230 | char* input = readline("lispy> "); 231 | add_history(input); 232 | 233 | printf("No you're a %s\n", input); 234 | free(input); 235 | 236 | } 237 | 238 | return 0; 239 | } 240 | ``` 241 | 242 | ## 参考 243 | 244 | `prompt_unix.c` 245 | 246 | ```c 247 | #include 248 | #include 249 | 250 | #include 251 | #include 252 | 253 | int main(int argc, char** argv) { 254 | 255 | /* Print Version and Exit Information */ 256 | puts("Lispy Version 0.0.0.0.1"); 257 | puts("Press Ctrl+c to Exit\n"); 258 | 259 | /* In a never ending loop */ 260 | while (1) { 261 | 262 | /* Output our prompt and get input */ 263 | char* input = readline("lispy> "); 264 | 265 | /* Add input to history */ 266 | add_history(input); 267 | 268 | /* Echo input back to user */ 269 | printf("No you're a %s\n", input); 270 | 271 | /* Free retrived input */ 272 | free(input); 273 | 274 | } 275 | 276 | return 0; 277 | } 278 | ``` 279 | 280 | `prompt_windows.c` 281 | ```c 282 | #include 283 | 284 | /* Declare a buffer for user input of size 2048 */ 285 | static char input[2048]; 286 | 287 | int main(int argc, char** argv) { 288 | 289 | /* Print Version and Exit Information */ 290 | puts("Lispy Version 0.0.0.0.1"); 291 | puts("Press Ctrl+c to Exit\n"); 292 | 293 | /* In a never ending loop */ 294 | while (1) { 295 | 296 | /* Output our prompt */ 297 | fputs("lispy> ", stdout); 298 | 299 | /* Read a line of user input of maximum size 2048 */ 300 | fgets(input, 2048, stdin); 301 | 302 | /* Echo input back to user */ 303 | printf("No you're a %s", input); 304 | } 305 | 306 | return 0; 307 | } 308 | ``` 309 | 310 | `prompt.c` 311 | ```c 312 | #include 313 | #include 314 | 315 | /* If we are compiling on Windows compile these functions */ 316 | #ifdef _WIN32 317 | #include 318 | 319 | static char buffer[2048]; 320 | 321 | /* Fake readline function */ 322 | char* readline(char* prompt) { 323 | fputs(prompt, stdout); 324 | fgets(buffer, 2048, stdin); 325 | char* cpy = malloc(strlen(buffer)+1); 326 | strcpy(cpy, buffer); 327 | cpy[strlen(cpy)-1] = '\0'; 328 | return cpy; 329 | } 330 | 331 | /* Fake add_history function */ 332 | void add_history(char* unused) {} 333 | 334 | /* Otherwise include the editline headers */ 335 | #else 336 | #include 337 | #include 338 | #endif 339 | 340 | int main(int argc, char** argv) { 341 | 342 | puts("Lispy Version 0.0.0.0.1"); 343 | puts("Press Ctrl+c to Exit\n"); 344 | 345 | while (1) { 346 | 347 | /* Now in either case readline will be correctly defined */ 348 | char* input = readline("lispy> "); 349 | add_history(input); 350 | 351 | printf("No you're a %s\n", input); 352 | free(input); 353 | 354 | } 355 | 356 | return 0; 357 | } 358 | ``` 359 | 360 | ## 彩蛋 361 | 362 | - 将提示信息 `lispy>` 换成其他你喜欢的。 363 | - 修改打印的信息。 364 | - 在程序开头的提示信息中添加一些其他的信息。 365 | - 在字符串中,`\n` 表示什么? 366 | - `printf` 还有哪些输出模式? 367 | - 如果你向 `printf` 传递一个与模式不匹配的值会怎样? 368 | - 预处理器 `#ifndef` 有什么用? 369 | - 预处理器 `#define` 有什么用? 370 | - `_WIN32` 在 Windows 中有定义,那在 Linux 和 Mac 中定义了什么呢? 371 | -------------------------------------------------------------------------------- /Introduction.md: -------------------------------------------------------------------------------- 1 | # 第零一章 • 介绍 2 | 3 | ## 这本书是给谁看的 4 | 5 | 本书是为那些想学习 C 语言,或是想知道怎样构建自己的编程语言的人编写的。本书不适合作为第一本编程入门书籍。但是如果你之前有一点编程的基础,不管是什么语言,我相信你都会在本书中找到一些新奇而有趣的东西。 6 | 7 | 我会把本书写的尽量的通俗易懂,对初学者更加友好。但是初学者可能还是会感觉本书的内容很有挑战性。我们会遇到非常多的新概念,而且还会同时学习两门**截然不同**的编程语言哦! 8 | 9 | 在写代码遇到问题的时候,如果到网上寻求帮助,你可能会发现人们对你并不友好。他们很多时候并不是在帮助你解决问题,而是炫耀他们在这方面的“知之甚多”。他们可能会指责你的错误,其实潜台词就是“你不适合写代码,不要再让你的烂代码污染这个世界了”。 10 | 11 | 在几次类似的经历之后,你很可能就会心灰意冷,觉得自己**根本算不上一个程序员**,或是自己**压根儿不喜欢编程**。你可能曾经想过要编写一门属于自己的编程语言或是其它的有趣而富有挑战性的项目。但是后来你意识到,难度太大了,我根本就做不到。 12 | 13 | 对此,我只能感到抱歉。程序员们可能无礼、自大、傲慢、脾气暴躁,但是这些都无可指责。毕竟每天加班累的跟狗一样,自然需要一个地方发泄。但是你需要知道的是,没有人是天生就会的。每个人都曾经是初学者。都犯过跟你一样的低级错误。所以,请不要放弃这项富有创造力的工作,而是努力去改变这个世界! 14 | 15 | ## 为什么要学习 C 语言 16 | 17 | C 语言是世界上最流行的,最具有影响力的编程语言之一。Linux 操作系统就是用 C 语言写成的。Apple OS X 和 Microsoft Windows 中也大量的用到了 C 语言。C 语言甚至还被用于微型电脑中--你家的冰箱和汽车可能就运行着使用 C 语言写就的程序。在现代的软件开发中,软件形式的多样化使得 C 语言在很多方面并不是首选,但是学会 C 语言仍然是软件开发者的必备技能之一。 18 | 19 | C 语言是自由的。从 Unix,Linux 到自由软件运动,它都起到了举足轻重的作用。而 C 语言本身也同样是自由的,它将自己所有的东西和盘托出,不藏不掖,甚至包括了自己的缺点。它几乎不限制你做任何事情,即使那样做会发生糟糕的事情。它给了你足够多的选择,该怎么做就全看你自己了。 20 | 21 | 掌握了 C 语言,就会懂得什么才是强大,巧妙和自由。在电脑前动动指尖就会让世界更加美好。 22 | 23 | ## 怎样学习 C 语言 24 | 25 | C 语言是一门很难学的语言。它有很多陌生的概念,而且初学者很难理解透彻。本书中,我不会去详细的介绍 C 语言的语法规则,或是循环和条件语句的编写方法之类的东西。我会告诉你的是构建真实世界中的 C 语言程序的方法。这种方式对于读者来说通常更加难以理解,但是却会教给你很多传统方法给不了的东西。本书并不能保证让你成为 C 语言的专家,但至少会让你学会一些有实际意义的知识,而不是学习一些没有意义的程序片段。 26 | 27 | 本书包含了 17 个短小精练的章节。你应该安排好你的阅读进度,每天按照计划阅读并及时实践。建议每天至少阅读一个章节。读完本书之后,你还可以继续完善你的 Lisp,使它更加完整与强大。 28 | 29 | ## 你自己的 Lisp 30 | 31 | 阅读学习本书的最好方式正如同本书的书名,Build Your Own Lisp。如果你学起来比较轻松,我建议你尝试添加或修改已有代码,为语言添加新的特性。虽然每一章都会有详细解释,但我还是给出了大量的代码示例。我知道很多人会直接将代码拷贝到自己的项目中,但是这样做通常不如自己亲自将代码写出来更加印象深刻。所以,*请不要这样做!* 32 | -------------------------------------------------------------------------------- /Languages.md: -------------------------------------------------------------------------------- 1 | # 第零五章 • 编程语言 2 | 3 | ## 什么是编程语言? 4 | 5 | 编程语言和自然语言非常相似,也有它背后固有的结构和规则来界定语句的正确性。当我们读写自然语言时,语言的规则就在无意中学会了。学习编程语言也是一样,需要长久的读写练习才能掌握。一旦掌握,我们就可以利用这些规则去理解其他人的代码,并写出自己的代码。 6 | 7 | 在 19 世纪 50 年代,语言学家 Noam Chomsky 定义了一系列关于语言的[重要理论](http://en.wikipedia.org/wiki/Chomsky_hierarchy)。这些理论支撑了我们今天对于语言结构的基本理解。其中重要的一条结论就是:自然语言都是建立在递归和重复的子结构之上的。 8 | 9 | 举例来说: 10 | 11 | > `The cat walked on the carpet.` 12 | 13 | 根据英语的规则,名词 `cat` 可以被两个由 `and` 连接的名词代替: 14 | 15 | > `The cat and dog walked on the carpet.` 16 | 17 | 我们可以像之前一样再次使用这个规则,将 `cat` 替换为两个使用 `and` 符号连接的新名词。我们还可以使用另外一个规则,将一个名词替换为一个形容词加一个名词,其中形容词作为对名词的修饰: 18 | 19 | > `The cat and mouse and dog walked on the carpet.` 20 | 21 | > `The white cat and black dog walked on the carpet.` 22 | 23 | 以上,我们只是简单的举两个例子。英语的语法规则远不止于此,汉语的语法规则就更复杂了,呵呵。 24 | 25 | 我们注意到,在编程语言中也有相似的规则。在 C 语言中,`if` 语句可以包含多条的新语句,新语句当然也可以是另一个 `if`语句。这些递归和重复的规则在语言的其他部分也同样是适用的。 26 | 27 | > `if (x > 5) { return x; }` 28 | 29 | > `if (x > 5) { if (x > 10) { return x; } }` 30 | 31 | Chomsky 提出的理论是非常重要的。它意味着,虽然一门语言可以表达无限的内容,我们仍然可以使用有限的规则去解析所有用该门语言写就的东西。这些有限的规则就叫语法(grammar)。 32 | 33 | 对于语法,我们有多种表达方式。最容易想到的方式就是使用白话文。譬如,我们可以这样说:*"句子必须是动词短语"*、*"动词词组可以是动词,也可以是副词加动词"* 等等 ( 译注:事实上,这也是我们最初学习英语语法的主要方式 )。这种形式对于人类来说是非常容易理解的,但是对于计算机却太模糊的、难以理解的。所以在写程序时,我们需要对语法有一个更标准化的描述。 34 | 35 | 为了定义一门编程语言(例如我们将要编写的 `Lisp`),我们首先需要能够正确解析用户按照语法规则写就的程序。为此需要编写一个*语法解析器*,用来判断用户的输入是否合法,并产生解析后的内部表示。内部表示是一种计算机更容易理解的表示形式,有了它,我们后面的解析、求值等工作会变得更加的简单可行。 36 | 37 | 但是这一部分往往是枯燥繁琐的体力活,我们显然不希望在这上面浪费时间。所以我们就采用了一个叫做 `mpc` 的库来帮助我们完成工作。 38 | 39 | ## 解析器组合子 40 | 41 | `mpc` 是我(原作者)编写的一个解析器组合子(Parser Combinators)库。这意味着,你可以使用这个库为任何语言编写语法解析器。编写语法解析器的方法有很多,使用解析器组合子的好处就在于,它极大地简化了原本枯燥无聊的工作,而仅仅编写高层的抽象语法规则就可以了。 42 | 43 | 50 | 51 | ## 编写语法规则 52 | 53 | 下面我们来编写一个柴犬语( [Doge](http://knowyourmeme.com/memes/doge) )的语法解析器以便熟悉 `mpc` 的用法。 54 | 55 | 56 | 先来看一下 `Doge` 语言的语法描述: 57 | 58 | - 形容词 (`Adjective`) 包括 `wow`、`many`、`so`、`such` 符号。 59 | - 名词 (`Noun`) 包括 `lisp`、`language`、`c`、`book`、`build` 符号。 60 | - 短语 (`Phrase`) 由形容词 (`Adjective`) 后接名词 (`Noun`) 组成。 61 | - `Doge` 语言由 0 到多个 短语(`Phrase`) 组成。 62 | 63 | 现在我们尝试定义一下形容词和名词,为此我们创建两个解析器,类型是 `mpc_parser_t*`,然后将解析器存储在 `Adjective` 和 `Noun` 两个变量中。`mpc_or` 函数产生一个解析器,它可接受的语句必须是指定语句中的一个。而 `mpc_sym` 将字符串转化为一个语句。 64 | 65 | 下面的代码也正如我们上面的描述一样: 66 | 67 | ```c 68 | /* Build a parser 'Adjective' to recognize descriptions */ 69 | mpc_parser_t* Adjective = mpc_or(4, 70 | mpc_sym("wow"), mpc_sym("many"), 71 | mpc_sym("so"), mpc_sym("such") 72 | ); 73 | 74 | /* Build a parser 'Noun' to recognize things */ 75 | mpc_parser_t* Noun = mpc_or(5, 76 | mpc_sym("lisp"), mpc_sym("language"), 77 | mpc_sym("book"),mpc_sym("build"), 78 | mpc_sym("c") 79 | ); 80 | ``` 81 | 82 | > #### 我怎样才能使用上面的这些 `mpc` 库提供的函数? 83 | 84 | > 现在先不用担心编译和运行程序的事情,先确保理解背后的理论知识。在下一章中我们将使用使用`mpc` 实现一个更加接近我们的 Lisp 的语言。 85 | 86 | 接下来,我们使用已经定义好的解析器 `Adjective` 、 `Noun` 来定义短语(`Phrase`)解析器。`mpc_and` 函数返回的解析器可接受的语句必须是各个语句按照顺序出现。所以我们将先前定义的 `Adjective` 和 `Noun` 传递给它,表示形容词后面紧跟名词组成短语。`mpcf_strfold` 和 `free` 指定了各个语句的组织及删除方式,我们可以暂时忽略它们。 87 | 88 | ```c 89 | mpc_parser_t* Phrase = mpc_and(2, mpcf_strfold, Adjective, Noun, free); 90 | ``` 91 | 92 | Doge 语言是由 0 到多个短语(Phrase) 组成的。`mpc_many` 函数表达的正是这种逻辑关系。同样的,我们可以暂时忽略 `mpcf_strfold` 参数。 93 | 94 | ```c 95 | mpc_parser_t* Doge = mpc_many(mpcf_strfold, Phrase); 96 | ``` 97 | 98 | 上述语句表明 Doge 可以接受任意多条语句。这也意味着 Doge 语言是无穷的。下面列出了一些符合 Doge 语法的例子: 99 | 100 | ``` 101 | "wow book such language many lisp" 102 | "so c such build such language" 103 | "many build wow c" 104 | "" 105 | "wow lisp wow c many language" 106 | "so c" 107 | ``` 108 | 109 | 我们可以继续使用 `mpc` 提供的其他函数,一步一步地编写能解析更加复杂的语法的解析器。相应地,随着复杂度的增加,代码的可读性也会越来越差。所以,这种写法其实并不简单。`mpc` 还提供了一系列的帮助函数来帮助用户更加简单地完成常见的任务,具体的文档说明可以参见[项目主页](http://github.com/orangeduck/mpc)。使用这些函数能够更好更快地构建复杂语言的解析器,并能够提供更加精细地控制。 110 | 111 | ## 更加自然的语法规则 112 | 113 | `mpc` 允许我们使用一种更加自然的方式来编写语法规则。我们可以将整个语言的语法规则写在一个长字符串中,而不是使用啰嗦难懂的 C 语句。我们也不再需要关心如何 使用 `mpcf_strfold` 或是 `free` 参数组织或删除各个语句。所有的这些工作都是都是自动完成的。 114 | 115 | 下面,我们使用这个方法重新编写了上面实现过的 Doge 语言: 116 | 117 | ```c 118 | mpc_parser_t* Adjective = mpc_new("adjective"); 119 | mpc_parser_t* Noun = mpc_new("noun"); 120 | mpc_parser_t* Phrase = mpc_new("phrase"); 121 | mpc_parser_t* Doge = mpc_new("doge"); 122 | 123 | mpca_lang(MPCA_LANG_DEFAULT, 124 | " \ 125 | adjective : \"wow\" | \"many\" \ 126 | | \"so\" | \"such\"; \ 127 | noun : \"lisp\" | \"language\" \ 128 | | \"book\" | \"build\" | \"c\"; \ 129 | phrase : ; \ 130 | doge : *; \ 131 | ", 132 | Adjective, Noun, Phrase, Doge); 133 | 134 | /* Do some parsing here... */ 135 | 136 | mpc_cleanup(4, Adjective, Noun, Phrase, Doge); 137 | ``` 138 | 139 | 即使你现在暂时不理解上面的长字符串的语法规则,也能明显地感觉到这个方法要比之前的清晰的多。下面就来具体的学习一下其中的某些特殊符号的意义及用法。 140 | 141 | 注意到,现在定义一个语法规则分为两个步骤: 142 | 1. 使用 `mpc_new` 函数定义语法规则的名字。 143 | 2. 使用 `mpca_lang` 函数具体定义这些语法规则。 144 | 145 | `mpca_lang` 函数的第一个参数是操作标记,在这里我们使用默认选项。第二个参数是 C 语言的一个长字符串。这个字符串中定义了具体的语法。它包含一系列的递归规则。每个规则分为两部分,用冒号 `:` 隔开,冒号左边是规则的名字,右边是规则的定义,使用 `;` 表示规则结束。 146 | 147 | 定义语法规则的一些特殊符号的作用如下: 148 | 149 | | 语法表示 | 作用 | 150 | |------------|-------------------------| 151 | | `"ab"` | 要求字符串 `ab` | 152 | | `'a'` | 要求字符 `a` | 153 | | `'a' 'b'` | 要求先有一个字符 `a`,后面紧跟一个字符 `b` | 154 | | `'a'|'b'` | 要求有字符 `a` 或字符 `b` | 155 | | `'a'*` | 要求有 0 个或多个字符 `a` | 156 | | `'a'+` | 要求有 1 个或多个字符 `a` | 157 | | `` | 要求满足名为 `abba` 定义的语法规则 | 158 | 159 | > #### 似曾相识的感觉? 160 | 161 | > 上面的一些语法规则有没有似曾相识的感觉?你没有猜错,`mpca_lang` 函数就是用 `mpc_many`、`mpc_and` 、 `mpc_or` 这些函数来实现的,干净利落,不拖泥带水。 162 | 163 | 根据表中给出的规则尝试着理解一下上面的代码,看看是不是等价于之前我们前面用代码定义过的语法解析器? 164 | 165 | 在后面的章节中,我们会使用这个方法来定义我们的语法规则。刚开始可能并不是那么容易理解,但随着时间的推移,我们练习的越来越多,你也将会更加熟悉,并将知道如何创建和编辑自己的语法规则。 166 | 167 | 本章更加注重的是理论知识,所以在做彩蛋部分时,不要太在意正确性,思考实现的方式才是最重要的。 168 | 169 | ## 参考 170 | 171 | `doge_code.c` 172 | 173 | ```c 174 | #include "mpc.h" 175 | 176 | int main(int argc, char** argv) { 177 | 178 | /* Build a parser 'Adjective' to recognize descriptions */ 179 | mpc_parser_t* Adjective = mpc_or(4, 180 | mpc_sym("wow"), mpc_sym("many"), 181 | mpc_sym("so"), mpc_sym("such") 182 | ); 183 | 184 | /* Build a parser 'Noun' to recognize things */ 185 | mpc_parser_t* Noun = mpc_or(5, 186 | mpc_sym("lisp"), mpc_sym("language"), 187 | mpc_sym("book"), mpc_sym("build"), 188 | mpc_sym("c") 189 | ); 190 | 191 | mpc_parser_t* Phrase = mpc_and(2, mpcf_strfold, 192 | Adjective, Noun, free); 193 | 194 | mpc_parser_t* Doge = mpc_many(mpcf_strfold, Phrase); 195 | 196 | /* Do some parsing here... */ 197 | 198 | mpc_delete(Doge); 199 | 200 | return 0; 201 | 202 | } 203 | ``` 204 | 205 | `doge_grammar.c` 206 | 207 | ```c 208 | #include "mpc.h" 209 | 210 | int main(int argc, char** argv) { 211 | 212 | mpc_parser_t* Adjective = mpc_new("adjective"); 213 | mpc_parser_t* Noun = mpc_new("noun"); 214 | mpc_parser_t* Phrase = mpc_new("phrase"); 215 | mpc_parser_t* Doge = mpc_new("doge"); 216 | 217 | mpca_lang(MPCA_LANG_DEFAULT, 218 | " \ 219 | adjective : \"wow\" | \"many\" \ 220 | | \"so\" | \"such\"; \ 221 | noun : \"lisp\" | \"language\" \ 222 | | \"book\" | \"build\" | \"c\"; \ 223 | phrase : ; \ 224 | doge : *; \ 225 | ", 226 | Adjective, Noun, Phrase, Doge); 227 | 228 | /* Do some parsing here... */ 229 | 230 | mpc_cleanup(4, Adjective, Noun, Phrase, Doge); 231 | 232 | return 0; 233 | 234 | } 235 | ``` 236 | 237 | ## 彩蛋 238 | 239 | - 为 Doge 语言的形容词和名词添加更多的字符串。 240 | - 为什么在定义语法规则的字符串中,`"` 前面要加 `\` 符号? 241 | - 为什么在定义语法规则的字符串中的每行的结尾要加 `\` 符号? 242 | - 描述诸如 `0.01`、`52.221` 这种小数的语法规则。 243 | - 描述诸如`https://github.com/ksco` 这种 URL 的语法规则。 244 | - 尝试描述一些简单英语句子的语法规则,例如:`the cat sat on the mat`。 245 | - 使用更加正式的语言来描述前面三个问题。如 `|`、`*`、`+` 等符号语言。 246 | - 如果你熟悉 JSON,请尝试描述一下它的语法。 247 | -------------------------------------------------------------------------------- /Parsing.md: -------------------------------------------------------------------------------- 1 | # 第零六章 • 语法分析 2 | 3 | ## 波兰表达式 4 | 5 | 为了验证 `mpc` 的威力,本章我们尝试实现一个简单的语法解析器--[波兰表达式](en.wikipedia.org/wiki/Polish_notation),它是我们将要实现的 Lisp 的数学运算部分。波兰表达式也是一种数学标记语言,它的运算符在操作数的前面。 6 | 7 | 举例来说: 8 | 9 | | 普通表达式 | 波兰表达式 | 10 | |-----------------|------------------------| 11 | | `1 + 2 + 6` | `+ 1 2 6` | 12 | | `6 + (2 * 9)` | `+ 6 (* 2 9)` | 13 | | `(10 * 2) / (4 + 2)` | `/ (* 10 2) (+ 4 2)` | 14 | 15 | 现在,我们需要编写这种标记语言的语法规则。我们可以先用白话文来尝试描述它,而后再将其公式化。 16 | 17 | 我们观察到,波兰表达式总是以操作符开头,后面跟着操作数或其他的包裹在圆括号中的表达式。也就是说,“程序(`Program`)是由一个操作符(`Operator`)加上一个或多个表达式(`Expression`)组成的”,而 “表达式(`Expression`)可以是一个数字,或者是包裹在圆括号中的一个操作符(`Operator`)加上一个或多个表达式(`Expression`)”。 18 | 19 | 下面是一个更加规整的描述: 20 | 21 | | 名称 | 定义 | 22 | |-----------------|------------------------| 23 | | 程序(`Program`) | `开始输入` --> `操作符` --> `一个或多个表达式(Expression)` --> `结束输入` | 24 | | 表达式(Expression) | `数字、左括号 (、操作符` --> `一个或多个表达式` --> `右括号 )` | 25 | | 操作符(Operator) | `'+'、'-'、'*' 、 '/'` | 26 | | 数字(`Number`) | `可选的负号 -` --> `一个或多个 0 到 9 之间的字符` | 27 | 28 | ## 正则表达式 29 | 30 | 我们可以使用上一章学过的符号来表示大多数的规则,但是在表示*数字*和*程序*规则时可能会遇到一些问题。这些规则需要用到一些我们没有讲解过的符号。我们还不知道如何表达开始和结束输入、可选字符、字符范围等。 31 | 32 | 这些规则由正则表达式(Regular Expression)定义。正则表达式适合定义一些小型的语法规则,例如单词或是数字等。正则表达式不支持复杂的规则,但它清晰且精确地界定了输入是否符合规则。下面是正则表达式的基本规则: 33 | 34 | | 语法表示 | 作用 | 35 | |------------|-------------------------| 36 | | `.` | 要求任意字符 | 37 | | `a` | 要求字符 `a` | 38 | | `[abcdef]` | 要求 `abcdef` 中的任意一个 | 39 | | `[a-f]` | 要求按照字母顺序,`a` 到 `f` 中的任意一个 | 40 | | `a?` | 要求 `a` 字符或什么都没有,即 `a` 为可选的 | 41 | | `a*` | 要求有 0 个或多个字符 `a` | 42 | | `a+` | 要求有 1 个或多个字符 `a` | 43 | | `^` | 开始输入 | 44 | | `$` | 结束输入 | 45 | 46 | 上面是我们目前需要的一些基本规则。如果你对正则表达式感兴趣,[这里](http://regex.learncodethehardway.org/)是关于它的一个完整的教程。 47 | 48 | 在 `mpc` 中,我们需要将正则表达式包裹在一对 `/` 中。例如,Number 可以用 `/-?[0-9]+/` 来表示。 49 | 50 | ## 安装 mpc 51 | 52 | 在我们正式编写这个语法解析器之前,正如之前在 Linux 和 Mac 上使用 `editline` 库一样,首先需要包含 `mpc` 的头文件,然后链接 `mpc` 库。 53 | 54 | 你可以直接使用第四章的代码,并将源文件重命名为 `parsing.c`,然后从 `mpc` 的[项目主页](http://github.com/orangeduck/mpc) 下载 `mpc.h` 和 `mpc.c`,放到和你的 `parsing.c` 同目录下。 55 | 56 | 在 `parsing.c` 的顶部添加 `#include "mpc.h"` 将 `mpc` 包含进来。将 `mpc.c` 放到命令行中来链接它。另外,在 Linux 上,还要加一个 -lm 参数来链接数学库。 57 | 58 | Mac 和 Linux: 59 | 60 | `cc -std=c99 -Wall parsing.c mpc.c -ledit -lm -o parsing` 61 | 62 | Windows: 63 | 64 | `cc -std=c99 -Wall parsing.c mpc.c -o parsing` 65 | 66 | > #### 等一下,包含头文件难道不是用 `#include `? 67 | 68 | > 事实上,在 C 语言中有两种包含头文件的方式,一种是用尖括号 `<>`,还有一种是用 `""` 双引号。通常,尖括号用来包含系统头文件如 `stdio.h`,双引号用来包含其他的头文件如 `mpc.h`。 69 | 70 | ## 波兰表达式语法解析 71 | 72 | 本节把白话文叙述的规则用正式的描述语言编写,并在必要的地方使用正则表达式。下面就是波兰表达式最终的语法规则。认真阅读下方代码,验证其是否与之前叙述的规则相符。 73 | 74 | ```c 75 | /* Create Some Parsers */ 76 | mpc_parser_t* Number = mpc_new("number"); 77 | mpc_parser_t* Operator = mpc_new("operator"); 78 | mpc_parser_t* Expr = mpc_new("expr"); 79 | mpc_parser_t* Lispy = mpc_new("lispy"); 80 | 81 | /* Define them with the following Language */ 82 | mpca_lang(MPCA_LANG_DEFAULT, 83 | " \ 84 | number : /-?[0-9]+/ ; \ 85 | operator : '+' | '-' | '*' | '/' ; \ 86 | expr : | '(' + ')' ; \ 87 | lispy : /^/ + /$/ ; \ 88 | ", 89 | Number, Operator, Expr, Lispy); 90 | ``` 91 | 92 | 我们还需要将代码放入第四章编写的交互式程序。将上面的代码放在 `main` 函数的开头处,打印版本信息的代码之前。另外,在 `main` 函数的最后,还应该将使用完毕的解析器删除。只需要将下面的代码放在 `return` 语句之前即可。 93 | 94 | /* Undefine and Delete our Parsers */ 95 | mpc_cleanup(4, Number, Operator, Expr, Lispy); 96 | 97 | > #### 编译的时候得到一个错误:`undefined reference to 'mpc_lang'` 98 | 99 | > 注意函数的名字为 `mpca_lang`,`mpc` 后面有个 `a` 字母。 100 | 101 | ## 解析用户输入 102 | 103 | 上面的代码为波兰表达式创建了一个 `mpc` 的解析器,本节我们就使用它来解析用户的每一条输入。我们需要更改之前的 `while` 循环,使它不再只是简单的将用户输入的内容打印回去,而是传进我们的解析器进行解析。我们可以把之前的 `printf` 语句替换成下面的代码: 104 | 105 | ```c 106 | /* Attempt to Parse the user Input */ 107 | mpc_result_t r; 108 | if (mpc_parse("", input, Lispy, &r)) { 109 | /* On Success Print the AST */ 110 | mpc_ast_print(r.output); 111 | mpc_ast_delete(r.output); 112 | } else { 113 | /* Otherwise Print the Error */ 114 | mpc_err_print(r.error); 115 | mpc_err_delete(r.error); 116 | } 117 | ``` 118 | 119 | 我们调用了 `mpc_parse` 函数,并将 `Lispy` 解析器和用户输入 `input` 作为参数。它将解析的结果保存到 `&r` 中,如果解析成功,返回值为 `1`,失败为 `0`。对 `r`,我们使用了取地址符 `&`,关于这个符号我们会在后面的章节中讨论。 120 | 121 | - 解析成功时会产生一个内部结构,并保存到 `r` 的 `output` 字段中。我们可以使用 `mpc_ast_print` 将这个结构打印出来,使用 `mpc_ast_delete` 将其删除。 122 | - 解析失败时则会将错误信息保存在 `r` 的 `error` 字段中。我们可以使用 `mpc_err_print` 将这个结构打印出来,使用 `mpc_err_delete` 将其删除。 123 | 124 | 重新编译程序,尝试不同的输入,看看程序的返回信息是什么。正常情况下返回值应该如下所示: 125 | 126 | ``` 127 | Lispy Version 0.0.0.0.2 128 | Press Ctrl+c to Exit 129 | 130 | lispy> + 5 (* 2 2) 131 | > 132 | regex 133 | operator|char:1:1 '+' 134 | expr|number|regex:1:3 '5' 135 | expr|> 136 | char:1:5 '(' 137 | operator|char:1:6 '*' 138 | expr|number|regex:1:8 '2' 139 | expr|number|regex:1:10 '2' 140 | char:1:11 ')' 141 | regex 142 | lispy> hello 143 | :1:1: error: expected whitespace, '+', '-', '*' or '/' at 'h' 144 | lispy> / 1dog 145 | :1:4: error: expected one of '0123456789', whitespace, '-', one or more of one of '0123456789', '(' or end of input at 'd' 146 | lispy> 147 | ``` 148 | 149 | > #### 编译的时候得到一个错误:`:1:1: error: Parser Undefined!` 150 | 151 | > 出现这个错误说明传给 `mpca_lang` 函数的语法规则存在错误,请仔细检查一下出错的地方。 152 | 153 | ## 参考 154 | 155 | `parsing.c` 156 | 157 | ```c 158 | #include "mpc.h" 159 | 160 | #ifdef _WIN32 161 | 162 | static char buffer[2048]; 163 | 164 | char* readline(char* prompt) { 165 | fputs(prompt, stdout); 166 | fgets(buffer, 2048, stdin); 167 | char* cpy = malloc(strlen(buffer)+1); 168 | strcpy(cpy, buffer); 169 | cpy[strlen(cpy)-1] = '\0'; 170 | return cpy; 171 | } 172 | 173 | void add_history(char* unused) {} 174 | 175 | #else 176 | #include 177 | #include 178 | #endif 179 | 180 | int main(int argc, char** argv) { 181 | 182 | /* Create Some Parsers */ 183 | mpc_parser_t* Number = mpc_new("number"); 184 | mpc_parser_t* Operator = mpc_new("operator"); 185 | mpc_parser_t* Expr = mpc_new("expr"); 186 | mpc_parser_t* Lispy = mpc_new("lispy"); 187 | 188 | /* Define them with the following Language */ 189 | mpca_lang(MPCA_LANG_DEFAULT, 190 | " \ 191 | number : /-?[0-9]+/ ; \ 192 | operator : '+' | '-' | '*' | '/' ; \ 193 | expr : | '(' + ')' ; \ 194 | lispy : /^/ + /$/ ; \ 195 | ", 196 | Number, Operator, Expr, Lispy); 197 | 198 | puts("Lispy Version 0.0.0.0.2"); 199 | puts("Press Ctrl+c to Exit\n"); 200 | 201 | while (1) { 202 | 203 | char* input = readline("lispy> "); 204 | add_history(input); 205 | 206 | /* Attempt to parse the user input */ 207 | mpc_result_t r; 208 | if (mpc_parse("", input, Lispy, &r)) { 209 | /* On success print and delete the AST */ 210 | mpc_ast_print(r.output); 211 | mpc_ast_delete(r.output); 212 | } else { 213 | /* Otherwise print and delete the Error */ 214 | mpc_err_print(r.error); 215 | mpc_err_delete(r.error); 216 | } 217 | 218 | free(input); 219 | } 220 | 221 | /* Undefine and delete our parsers */ 222 | mpc_cleanup(4, Number, Operator, Expr, Lispy); 223 | 224 | return 0; 225 | } 226 | ``` 227 | 228 | ## 彩蛋 229 | 230 | - 使用正则表达式编写规则,使其可以解析任意多个 `a`、`b` 组成的字符串,例如: `aababa`、`bbaa`。 231 | - 使用正则表达式编写规则,使其可以解析任意多个交替出现的 `a`、`b` 组成的字符串,例如: `ababab`、`aba`。 232 | - 修改语法规则,添加新的操作符,例如取余运算符 `%`。 233 | - 修改语法规则,使其可以解析用文本形式的操作符,例如:`add`、`sub`、`mul`、`div`等。 234 | - 修改语法规则,使其能够处理浮点数,例如:`0.01`、`5.21`、`10.2`。 235 | -------------------------------------------------------------------------------- /Q-Expressions.md: -------------------------------------------------------------------------------- 1 | # 第零十章 • Q-表达式 2 | 3 | ## 添加特性 4 | 5 | 你可能会注意到包括本章在内的之后章节都遵循一个模式,这个模式也是给一个编程语言添加新特性的典型方式。它包含一系列的步骤来从无到有的实现某个特性。下表详细地说明了本章所要引入的 Q-表达式的具体实现步骤。 6 | 7 | | 名称 | 描述 | 8 | | ------- | ------------------------- | 9 | | **语法** | 为新特性添加新的语法规则 | 10 | | **表示** | 为新特性添加新的数据类型 | 11 | | **解析** | 为新特性添加新的函数,正确处理 AST | 12 | | **语义** | 为新特性添加新的函数,用于求值和操作 | 13 | 14 | ## Q-表达式 15 | 16 | 本章我们将实现一个新的 Lisp 值类型,叫做 Q-表达式。 17 | 18 | 它的英文全称为 *quoted expression*,跟 S-表达式一样,也是 Lisp 表达式的一种,但它不受标准 Lisp 求值机制的作用。也就是说,当受到函数的作用时,Q-表达式不会被求值,而是保持原样。这个特性让 Q-表达式有着广泛的应用。我们可以用它来存储和管理其他的 Lisp 值类型,例如数字、符号或 S-表达式等。 19 | 20 | 在添加 Q-表达式之后,我们还需要定义一系列的操作来管理它。类似于数学操作,这些操作定义了 Q-表达式具体的行为。 21 | 22 | Q- 表达式的语法和 S-表达式非常相似,唯一的不同是 Q-表达式包裹在大括号 `{}` 中,而非 S-表达式的小括号 `()`,Q-表达式的语法规则如下所示。 23 | 24 | > #### 我从来没听说过 Q-表达式 25 | > 好吧,其实 Q-表达式不存在于其它的 Lisp 方言中,它们通常使用宏来禁止表达式求值。宏看起来类似于普通的函数,但不会对参数进行求值。有一个特殊叫做引用(`'`)的宏,可以用来禁止几乎所有表达式的求值,这个宏也是本书中 Q-表达式的灵感来源。所以 Q-表达式是 Lispy 独有的,我们用它来替代宏完成相应的任务。 26 | > 27 | > 本书中的 S-表达式和 Q-表达式有滥用概念的嫌疑,但我希望这些“不恰当的行为”能够使我们的 Lispy 的行为更加清晰简洁。 28 | 29 | ```c 30 | mpc_parser_t* Number = mpc_new("number"); 31 | mpc_parser_t* Symbol = mpc_new("symbol"); 32 | mpc_parser_t* Sexpr = mpc_new("sexpr"); 33 | mpc_parser_t* Qexpr = mpc_new("qexpr"); 34 | mpc_parser_t* Expr = mpc_new("expr"); 35 | mpc_parser_t* Lispy = mpc_new("lispy"); 36 | 37 | mpca_lang(MPCA_LANG_DEFAULT, 38 | " \ 39 | number : /-?[0-9]+/ ; \ 40 | symbol : '+' | '-' | '*' | '/' ; \ 41 | sexpr : '(' * ')' ; \ 42 | qexpr : '{' * '}' ; \ 43 | expr : | | | ; \ 44 | lispy : /^/ * /$/ ; \ 45 | ", 46 | Number, Symbol, Sexpr, Qexpr, Expr, Lispy); 47 | ``` 48 | 49 | 另外,不要忘记同步更新清理函数 `mpc_cleanup` 来处理我们新添加的规则。 50 | 51 | ```c 52 | mpc_cleanup(6, Number, Symbol, Sexpr, Qexpr, Expr, Lispy); 53 | ``` 54 | 55 | ## 读取 Q-表达式 56 | 57 | 由于 Q-表达式和 S-表达式的形式基本一致,所以它们内部实现也大致是相同的。我们考虑重用 S-表达式的数据结构来表示 Q-表达式,在此之前需要向枚举中添加一个单独的类型。 58 | 59 | ```c 60 | enum { LVAL_ERR, LVAL_NUM, LVAL_SYM, LVAL_SEXPR, LVAL_QEXPR }; 61 | ``` 62 | 另外,还需为其编写一个构造函数。 63 | 64 | ```c 65 | /* A pointer to a new empty Qexpr lval */ 66 | lval* lval_qexpr(void) { 67 | lval* v = malloc(sizeof(lval)); 68 | v->type = LVAL_QEXPR; 69 | v->count = 0; 70 | v->cell = NULL; 71 | return v; 72 | } 73 | ``` 74 | 75 | Q-表达式的打印和删除逻辑也和 S-表达式别无二致,我们只需照葫芦画瓢,在相应的函数中添加对应的逻辑即可,具体如下所示。 76 | 77 | ```c 78 | void lval_print(lval* v) { 79 | switch (v->type) { 80 | case LVAL_NUM: printf("%li", v->num); break; 81 | case LVAL_ERR: printf("Error: %s", v->err); break; 82 | case LVAL_SYM: printf("%s", v->sym); break; 83 | case LVAL_SEXPR: lval_expr_print(v, '(', ')'); break; 84 | case LVAL_QEXPR: lval_expr_print(v, '{', '}'); break; 85 | } 86 | } 87 | ``` 88 | 89 | ```c 90 | void lval_del(lval* v) { 91 | 92 | switch (v->type) { 93 | case LVAL_NUM: break; 94 | case LVAL_ERR: free(v->err); break; 95 | case LVAL_SYM: free(v->sym); break; 96 | 97 | /* If Qexpr or Sexpr then delete all elements inside */ 98 | case LVAL_QEXPR: 99 | case LVAL_SEXPR: 100 | for (int i = 0; i < v->count; i++) { 101 | lval_del(v->cell[i]); 102 | } 103 | /* Also free the memory allocated to contain the pointers */ 104 | free(v->cell); 105 | break; 106 | } 107 | 108 | free(v); 109 | } 110 | ``` 111 | 112 | 经过这些简单的变化之后,我们就可以更新读取函数 `lval_read`,使其可以正确读取 Q-表达式了。因为 Q-表达式重用了所有 S-表达式的数据类型,所以我们也自然可以重用所有 S-表达式的函数,例如 `lval_add`。 113 | 114 | 因此,为了能够读取 Q-表达式,我们只需在抽象语法树中检测并创建空的 S-表达式的地方添加一个新的情况即可。 115 | 116 | ```c 117 | if (strstr(t->tag, "qexpr")) { x = lval_qexpr(); } 118 | ``` 119 | 120 | 因为 Q-表达式没有任何求值方式,所以无需改动任何已有的求值函数,我们的 Q-表达式就可以小试牛刀了。尝试输入几个 Q-表达式,看看是否不会被求值。 121 | 122 | ```c 123 | lispy> {1 2 3 4} 124 | {1 2 3 4} 125 | lispy> {1 2 (+ 5 6) 4} 126 | {1 2 (+ 5 6) 4} 127 | lispy> {{2 3 4} {1}} 128 | {{2 3 4} {1}} 129 | lispy> 130 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 第零零章 • 关于 2 | 3 | 在本书中,你将在学习 C 语言的同时学会编写你自己的编程语言——一个 1000 行左右代码的简单 Lisp。不过我们并不是从零开始编写的,在代码中我用到了一个外部的库来完成一些初始化的工作。但是剩下的最重要的部分都是我们一行一行编写的,而且在本书结束时,你将会拥有一个属于自己的“麻雀虽小,五脏俱全”的 Lisp。 4 | 5 | 很多人非常想学习 C 语言,但却无从下手。现在大可不必担心了。如果你能坚持看完本书,我敢保证,至少你将拥有一个非常酷的新语言可以把玩,说不定还能成为一个熟练的 C 程序员呢! 6 | 7 | ### 原作者(Author):Daniel Holden (contact@theorangeduck.com) 8 | - 阅读地址:http://buildyourownlisp.com/ 9 | - 项目主页:[BuildYourOwnLisp](https://github.com/orangeduck/BuildYourOwnLisp) 10 | - 共享协议:[CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) 11 | 12 | ### 译者(Translator):KSCO (numbksco@gmail.com) 13 | - 阅读地址:https://ksco.gitbooks.io/build-your-own-lisp/ 14 | - 项目主页:[BuildYourOwnLispCn](https://github.com/ksco/BuildYourOwnLispCn) 15 | - 共享协议:[CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) 16 | -------------------------------------------------------------------------------- /S-Expressions.md: -------------------------------------------------------------------------------- 1 | # 第零九章 • S-表达式 2 | 3 | ## Lisp 列表 4 | 5 | Lisp 程序代码与数据的形式完全相同,这使得它非常强大,能完成许多其他语言不能完成的事情。为了拥有这个强大的特性,我们需要将求值过程分为读取并存储输入、对输入进行求值两个过程。 6 | 7 | 本章结束后,程序的运行结果会和上一章的有轻微的不同。这是因为我们会花时间去更改程序内部的工作方式。在软件开发中,这被叫做**重构**。重构可能对于当前的程序运行结果并没有太大的影响,但因为工作方式的优化,会使我们在后面的开发中更加省心。 8 | 9 | 为了存储输入,我们需要创建一个内部列表结构,能够递归地表示数字、操作符号以及其他的列表。在 Lisp 中,这个结构被称为 S-表达式(Symbolic Expression)。我们将扩展 `lval` 结构来表示它。S-表达式求值也是典型的 Lisp 式过程:首先取列表第一个元素为操作符,然后遍历所有剩下的元素,将它们作为操作数。 10 | 11 | 有了 S-表达式,我们才算真正迈进了 Lisp 的大门。 12 | 13 | ## 指针 14 | 15 | 在 C 语言中,要表示列表,就必须正确的使用指针。C 语言中的指针一直如洪水猛兽般存在。虽然概念上非常简单,但是用起来却变幻多端,神秘莫测,这使得指针看上去比实际要可怕得多。幸运的是,在本书中我们只会用一些指针在 C 语言中最常规的用法。 16 | 17 | 我们之所以需要指针,主要是由 C 语言中函数的工作方式决定的。C 语言函数的参数**全部**是通过值传递的。也就是说,传递给函数的实际是实参的拷贝。对于 `int`、`long`、`char`等系统类型以及用户自定义的结构体都是成立的。这种方式适用于绝大多数情况,但也会偶尔出现问题。 18 | 19 | 一种常见的情况是,如果我们有一个巨大结构体需要作为参数传递,则每次调用函数,就会对实参进行一次拷贝,这无疑是对性能和内存的浪费。 20 | 21 | 另外一个问题是,结构体的大小终究是有限的,无论多大,也只能是个固定的大小。而如果我们想向函数传递一组数据,而且数据的总数还是不固定的,结构体就明显的无能为力了。 22 | 23 | 为了解决这个问题,C 语言的开发者们想出了一个聪明的办法。他们把内存想象成一个巨大的字节数组,每个字节都可以拥有一个全局的索引值。这有点像门牌号:第一个字节索引为 0,第二个字节索引为 1,等等。 24 | 25 | 在这种情况下,计算机中的所有数据,包括当前运行的程序中的结构体、变量都有相应的索引值与其对应(数据的开始字节的索引作为整个数据的索引)。所以,除了将数据本身拷贝到函数参数,我们还可以只拷贝数据的索引值。在函数内部则可以根据索引值找到需要的数据本身(译者注:我们将这个索引值称为*地址*,存储地址的变量称为*指针*)。使用指针,函数可以修改指定位置的内存而无需拷贝。除此之外,指针还可以做其他很多事情。 26 | 27 | 因为计算机内存的大小是固定的,表示一个地址所需要的字节数也是固定的。但是地址指向的内存的字节数是可以变化的。这就意味着,我们可以创建一个大小可变的数据结构,并将其指针传入函数,对其进行读取及修改。 28 | 29 | 所以,所谓的指针也仅仅是一个数字而已。是内存中的一块数据的开始字节的索引值。指针的类型用来提示程序员和编译器指针指向的是一块什么样的数据,占多少个字节等。 30 | 31 | 指针类型是在现有类型的后面加一个星号组成,我们之前已经见过一些指针的示例了,如:`mpc_parser_t*`、`mpc_ast_t*` 以及 `char*`。 32 | 33 | 要创建指针,我们就需要获取数据的地址。C 语言提供了取地址符(`&`)来获取某个数据的地址。在前面的章节中,我们也曾传给过 `mpc_parse` 函数一个指针,以便其能将输出放到我们声明的 `mpc_result_t` 变量中。 34 | 35 | 最后,为了获取指针所指向的地址的数据值(称为*解引用*),我们需要在指针左边使用 `*` 操作符。要获取结构体指针的某个字段,需要使用 `->` 操作符,而不是 `.`,这你在第七章已经见过了。 36 | 37 | ## 栈(Stack)和堆(Heap) 38 | 39 | 前面说过,我们可以把内存简单粗暴地想象成一个巨大的字节数组。事实上,它被更加合理地划分成了两部分,即*栈*和*堆*。 40 | 41 | 有些人可能已经听说过一些关于堆和栈的神秘传说,例如“栈从上往下增长,而堆则是从下往上”,或是“栈的数量很多,但堆只有一个”云云。其实这些事情都是无关紧要的。在 C 语言中,处理好栈和堆确实是件麻烦的事情,但这并不代表它们很神秘。实际上,它们只是内存中的两块不同的区域,分别用来完成不同的任务而已。 42 | 43 | ### 栈(The Stack) 44 | 45 | 栈是程序赖以生存的地方,所有的临时变量和数据结构都保存于其中,供你读取及编辑。每次调用一个新的函数,就会有一块新的栈区压入,并在其中存放函数内的临时变量、传入的实参的拷贝以及其它的一些信息。当函数运行完毕,这块栈区就会被弹出并回收,供其他函数使用。 46 | 47 | 我喜欢把栈想象成一个建筑工地。每次需要干点新事情的时候,我们就圈出一块地方来,放工具、原料,并在这里工作。如果需要的话,我们也可以到工地的其他地方,甚至是离开工地。但是我们所有的工作都是在自己的地方完成的。一旦工作完成,我们就把工作成果转移到新的地方,并把现在工作的地方清理干净。 48 | 49 | ### 堆(The Heap) 50 | 51 | 堆占据另一部分内存,主要用来存放长生命周期期的数据。堆中的数据必须手动申请和释放。申请内存使用 `malloc` 函数。这个函数接受一个数字作为要申请的字节数,返回申请好的内存块的指针。 52 | 53 | 当使用完毕申请的内存,我们还需要将其释放,只要将 `malloc` 函数返回的指针传给 `free` 函数即可。 54 | 55 | 堆比栈的使用难度要大一些,因为它要求程序员手动调用 `free` 函数释放内存,而且还要正确调用。如果不释放,程序就有可能不断申请新的内存,而不释放旧的,导致内存越用越多。这也被称为*内存泄漏*。避免这种情况发生的一个简单有效的办法就是,针对每一个 `malloc` 函数调用,都有且只有一个 `free` 函数与之对应。这某种程度上就能保证程序能正确处理堆内存的使用。 56 | 57 | 我把堆想象成一个自助存储仓库,我们使用 `malloc` 函数申请存储空间。我们可以在自主存储仓库和建筑工地之间自由存取。它非常适合用来存放大件的偶尔才用一次的物件。唯一的问题就是在用完之后要记得使用 `free` 函数将空间归还。 58 | 59 | ## 解析表达式 60 | 61 | 因为现在我们考虑的是 S-表达式,而不是之前的波兰表达式了,我们需要更新一下语法分析器。S-表达式的语法非常简单。只是小括号之间包含一组表达式而已。而这些表达式可以是数字、操作符或是其他的 S-表达式。只需修改一下之前写的就可以了。另外,我们还需把 `operator` 规则重命名为 `symbol`。为之后添加更多的操作符以及变量、函数等做准备。 62 | 63 | ```c 64 | mpc_parser_t* Number = mpc_new("number"); 65 | mpc_parser_t* Symbol = mpc_new("symbol"); 66 | mpc_parser_t* Sexpr = mpc_new("sexpr"); 67 | mpc_parser_t* Expr = mpc_new("expr"); 68 | mpc_parser_t* Lispy = mpc_new("lispy"); 69 | 70 | mpca_lang(MPCA_LANG_DEFAULT, 71 | " \ 72 | number : /-?[0-9]+/ ; \ 73 | symbol : '+' | '-' | '*' | '/' ; \ 74 | sexpr : '(' * ')' ; \ 75 | expr : | | ; \ 76 | lispy : /^/ * /$/ ; \ 77 | ", 78 | Number, Symbol, Sexpr, Expr, Lispy); 79 | ``` 80 | 81 | 同时,还要记得在退出之前做好清理工作: 82 | 83 | ```c 84 | mpc_cleanup(5, Number, Symbol, Sexpr, Expr, Lispy); 85 | ``` 86 | 87 | ## 表达式结构 88 | 89 | 首先,我们需要让 `lval` 能够存储 S-表达式。这意味着我们还要能存储符号(Symbols)和数字。我们向枚举中添加两个新的类型。`LVAL_SYM` 表示操作符类型,例如 `+` 等,`LVAL_SEXPR` 表示 S-表达式。 90 | 91 | ```c 92 | enum { LVAL_ERR, LVAL_NUM, LVAL_SYM, LVAL_SEXPR }; 93 | ``` 94 | 95 | S-表达式是一个可变长度的列表。在本章的开头已经提到,我们不能创建可变长度的结构体,所以只能使用指针来表示它。我们为 `lval` 结构体创建一个 `cell` 字段,指向一个存放 `lval*` 列表的区域。所以 `cell` 的类型就应该是 `lval**`。指向 `lval*` 的指针。我们还需要知道 `cell` 列表中的元素个数,所以我创建了 `count` 字段。 96 | 97 | 我们使用字符串来表示符号(Symbols),另外我们还增加了另一个字符串用来存储错误信息。也就是说现在 `lval` 可以存储更加具体的错误信息了,而不只是一个错误代码,这使得我们的错误报告系统更加灵活好用。我们也可以删除掉之前写的错误枚举了。升级过后的 `lval` 结构体如下所示: 98 | 99 | ```c 100 | typedef struct lval { 101 | int type; 102 | long num; 103 | /* Error and Symbol types have some string data */ 104 | char* err; 105 | char* sym; 106 | /* Count and Pointer to a list of "lval*" */ 107 | int count; 108 | struct lval** cell; 109 | } lval; 110 | ``` 111 | 112 | > #### 有指向指向指针的指针的指针吗? 113 | 114 | > 这里有个古老的笑话,说是可以根据 C 程序员的程序中指针后面的星星数(`*`)作为其水平的评分。:P 115 | 116 | > 初级水平的人写的程序可能只会用到像 `char*` 或是奇怪的 `int*` 等一级指针,所以他们被称为一星程序员。而大多数中级的程序员则会用到诸如 `lval**` 这类的二级指针,所以他们被称为二星程序员。但据说能用三级指针的就真的很少见了,你可能会在一些伟大的作品中见到,这些代码的妙处凡夫俗子自然也是体会不到的。果真如此,三星程序员这个称号真是极大的赞誉了。 117 | > 118 | > 但据我所知,还没有人用到过四级指针。 119 | 120 | ## 构造函数和析构函数 121 | 122 | 我们可以重写 `lval` 的构造函数,使其返回 `lval` 的指针,而不是其本身。这样做会使得对 `lval` 变量进行跟踪更加简单。为此,我们需要用到 `malloc` 库函数以及 `sizeof` 操作符为 `lval` 结构体在堆上申请足够大的内存区域,然后使用 `->` 操作符填充结构体中的相关字段。 123 | 124 | 当我们构造一个 `lval` 时,它的某些指针字段可能会包含其他的在堆上申请的内存,所以我们应该小心行事。当某个 `lval` 完成使命之后,我们不仅需要删除它本身所指向的堆内存,还要删除它的字段所指向的堆内存。 125 | 126 | ```c 127 | /* Construct a pointer to a new Number lval */ 128 | lval* lval_num(long x) { 129 | lval* v = malloc(sizeof(lval)); 130 | v->type = LVAL_NUM; 131 | v->num = x; 132 | return v; 133 | } 134 | ``` 135 | 136 | ```c 137 | /* Construct a pointer to a new Error lval */ 138 | lval* lval_err(char* m) { 139 | lval* v = malloc(sizeof(lval)); 140 | v->type = LVAL_ERR; 141 | v->err = malloc(strlen(m) + 1); 142 | strcpy(v->err, m); 143 | return v; 144 | } 145 | ``` 146 | 147 | ```c 148 | /* Construct a pointer to a new Symbol lval */ 149 | lval* lval_sym(char* s) { 150 | lval* v = malloc(sizeof(lval)); 151 | v->type = LVAL_SYM; 152 | v->sym = malloc(strlen(s) + 1); 153 | strcpy(v->sym, s); 154 | return v; 155 | } 156 | ``` 157 | 158 | ```c 159 | /* A pointer to a new empty Sexpr lval */ 160 | lval* lval_sexpr(void) { 161 | lval* v = malloc(sizeof(lval)); 162 | v->type = LVAL_SEXPR; 163 | v->count = 0; 164 | v->cell = NULL; 165 | return v; 166 | } 167 | ``` 168 | 169 | > #### `NULL` 是什么? 170 | 171 | > `NULL` 是一个指向内存地址 0 的特殊常量。按照惯例,它通常被用来表示空值或无数据。在上面的代码中,我们使用 `NULL` 来表示虽然我们有一个数据指针,但它目前还没有指向任何内容。在本书的后续章节中你讲经常性地遇到这个特殊的常量,所以,请眼熟它。 172 | 173 | --- 174 | 175 | > #### 为什么要使用 `strlen(s) + 1`? 176 | 177 | > 在 C 语言中,字符串是以空字符做为终止标记的。所以,C 语言字符串的最后一个字符一定是 `\0`。请确保所有的字符串都是按照这个约定来存储的,不然程序就会因为莫名其妙的错误退出。`strlen` 函数返回的是字符串的实际长度(所以不包括结尾的 `\0` 终止符)。所以为了保证有足够的空间存储所有字符,我们需要在额外 +1。 178 | 179 | 现在,我们需要一个定制的函数来删除 `lval*`。这个函数应该调用 `free` 函数来释放本身所指向的由 `malloc` 函数所申请的内存。但更重要的是,它应该根据自身的类型,释放所有它的字段指向的内存。所以我们只需按照上方介绍的那些构造函数对应释放相应的字段,就不会有内存的泄露了。 180 | 181 | ```c 182 | void lval_del(lval* v) { 183 | 184 | switch (v->type) { 185 | /* Do nothing special for number type */ 186 | case LVAL_NUM: break; 187 | 188 | /* For Err or Sym free the string data */ 189 | case LVAL_ERR: free(v->err); break; 190 | case LVAL_SYM: free(v->sym); break; 191 | 192 | /* If Sexpr then delete all elements inside */ 193 | case LVAL_SEXPR: 194 | for (int i = 0; i < v->count; i++) { 195 | lval_del(v->cell[i]); 196 | } 197 | /* Also free the memory allocated to contain the pointers */ 198 | free(v->cell); 199 | break; 200 | } 201 | 202 | /* Free the memory allocated for the "lval" struct itself */ 203 | free(v); 204 | } 205 | ``` 206 | 207 | ## 读取表达式 208 | 209 | 首先我们会读取整个程序,并构造一个 `lval*` 来表示它,然后我们对这个 `lval*` 进行遍历求值来得到程序的运行结果。第一阶段负责把抽象语法树(abstract syntax tree)转换为一个 S-表达式,第二阶段则根据我们已由的 Lisp 规则对 S-表达式进行遍历求值。 210 | 211 | 为了完成第一步,我们可以递归的查看语法分析树中的每个节点,并根据节点的 `tag` 和 `contents` 字段构造出不同类型的 `lval*`。 212 | 213 | 如果给定节点的被标记为 `number` 或 `symbol`,则我们可以调用对应的构造函数直接返回一个 `lval*`。如果给定的节点被标记为 `root` 或 `sexpr`,则我们应该构造一个空的 S-表达式类型的 `lval*`,并逐一将它的子节点加入。 214 | 215 | 为了更加方便的像一个 S-表达式中添加元素,我们可以创建一个函数 `lval_add`,这个函数将表达式的子表达式计数加一,然后使用 `realloc` 函数为 `v->cell` 字段重新扩大申请内存,用于存储刚刚加入的子表达式 `lval* x`。 216 | 217 | 222 | 223 | ```c 224 | lval* lval_read_num(mpc_ast_t* t) { 225 | errno = 0; 226 | long x = strtol(t->contents, NULL, 10); 227 | return errno != ERANGE ? 228 | lval_num(x) : lval_err("invalid number"); 229 | } 230 | ``` 231 | 232 | ```c 233 | lval* lval_read(mpc_ast_t* t) { 234 | 235 | /* If Symbol or Number return conversion to that type */ 236 | if (strstr(t->tag, "number")) { return lval_read_num(t); } 237 | if (strstr(t->tag, "symbol")) { return lval_sym(t->contents); } 238 | 239 | /* If root (>) or sexpr then create empty list */ 240 | lval* x = NULL; 241 | if (strcmp(t->tag, ">") == 0) { x = lval_sexpr(); } 242 | if (strstr(t->tag, "sexpr")) { x = lval_sexpr(); } 243 | 244 | /* Fill this list with any valid expression contained within */ 245 | for (int i = 0; i < t->children_num; i++) { 246 | if (strcmp(t->children[i]->contents, "(") == 0) { continue; } 247 | if (strcmp(t->children[i]->contents, ")") == 0) { continue; } 248 | if (strcmp(t->children[i]->contents, "}") == 0) { continue; } 249 | if (strcmp(t->children[i]->contents, "{") == 0) { continue; } 250 | if (strcmp(t->children[i]->tag, "regex") == 0) { continue; } 251 | x = lval_add(x, lval_read(t->children[i])); 252 | } 253 | 254 | return x; 255 | } 256 | ``` 257 | 258 | ```c 259 | lval* lval_add(lval* v, lval* x) { 260 | v->count++; 261 | v->cell = realloc(v->cell, sizeof(lval*) * v->count); 262 | v->cell[v->count-1] = x; 263 | return v; 264 | } 265 | ``` 266 | 267 | ## 打印表达式 268 | 269 | 距离验证本章做的所有改变只有一步之遥了!我们还需要修改打印函数使其能够打印出 S-表达式。通过将 S-表达式打印出来和输入对比可以进一步检查读入模块是否正确工作。 270 | 271 | 为了打印 S-表达式,我们创建一个新函数,遍历所有的子表达式,并将它们单独打印出来,以空格隔开,正如输入的时候一样。 272 | 273 | ```c 274 | void lval_expr_print(lval* v, char open, char close) { 275 | putchar(open); 276 | for (int i = 0; i < v->count; i++) { 277 | 278 | /* Print Value contained within */ 279 | lval_print(v->cell[i]); 280 | 281 | /* Don't print trailing space if last element */ 282 | if (i != (v->count-1)) { 283 | putchar(' '); 284 | } 285 | } 286 | putchar(close); 287 | } 288 | ``` 289 | 290 | ```c 291 | void lval_print(lval* v) { 292 | switch (v->type) { 293 | case LVAL_NUM: printf("%li", v->num); break; 294 | case LVAL_ERR: printf("Error: %s", v->err); break; 295 | case LVAL_SYM: printf("%s", v->sym); break; 296 | case LVAL_SEXPR: lval_expr_print(v, '(', ')'); break; 297 | } 298 | } 299 | 300 | void lval_println(lval* v) { lval_print(v); putchar('\n'); } 301 | ``` 302 | 303 | > #### 我没法声明这些函数,因为它们互相调用了对方。 304 | 305 | > `lval_expr_print` 函数内部调用了 `lval_print` 函数,`lval_print` 内部又调用了 `lval_expr_print`。似乎是没有办法解决依赖性的。C 语言提供了*前置声明*来解决这个问题。前置声明只定义了函数的形式,而没有函数体(译者注:前置声明就是告诉编译器:“我保证有这个函数,你放心调用就是了”)。它允许其他函数调用它,而具体的函数定义则在后面。函数声明只要将函数定义的函数体换成 `;` 即可。在我们的程序中,应该将 `void lval_print(lval* v);` 语句放在一个比 `lval_expr_print` 函数靠前的地方。在以后的编程过程中,你一定会再次遇到这个问题,所以请记住这个解决方案! 306 | 307 | 在主循环中,我们可以将求值部分移除了,替换为新写就的读取和打印函数。 308 | 309 | ```c 310 | lval* x = lval_read(r.output); 311 | lval_println(x); 312 | lval_del(x); 313 | ``` 314 | 315 | 正常情况下,你应该可以看到下面这样的结果。 316 | 317 | ```c 318 | lispy> + 2 2 319 | (+ 2 2) 320 | lispy> + 2 (* 7 6) (* 2 5) 321 | (+ 2 (* 7 6) (* 2 5)) 322 | lispy> * 55 101 (+ 0 0 0) 323 | (* 55 101 (+ 0 0 0)) 324 | lispy> 325 | ``` 326 | 327 | ## 表达式求值 328 | 329 | 求值函数的具体行为和之前的并无太大变化。我们需要将其适配本章定义的 `lval*` 以及更加灵活的表达式定义。其实可以把求值函数想象成某种转换器--它读取 `lval*` 作为输入,通过某种方式将其转化为新的 `lval*` 并输出。在有些时候,求值函数不对输入做任何修改,原封不动的将其返回;有些时候,它会对输入的做一些改动;而在大多数情况下,它会将输入的 `lval*` 删除,返回完全不同的东西。如果要返回新的东西,一定要记得将原有的作为输入的 `lval*` 删除。 330 | 331 | 对于 S-表达式,我们首先遍历它所有的子节点,如果子节点有任何错误,我们就使用稍后定义的函数 `lval_take` 将遇到的第一个错误返回。 332 | 333 | 对于没有子节点的 S-表达式直接将其返回就可以了,这是为了处理空表达式 `{}` 的情况。另外,我们还需要检查只有一个子节点的表达式,例如 `{5}`,这种情况我们应该将其包含的表达式返回。 334 | 335 | 如果以上情况都不成立,那我们就知道这是一个合法的表达式,有个多于一个的子节点。对于此种情况,我们使用稍后定义的函数 `lval_pop` 将第一个元素从表达式中分离开来,然后检查确保它是一个 `symbol`。然后根据它的具体类型,将它和参数一起传入 `builtin_op` 函数计算求值。如果它不是 `symbol`,我们就将它以及传进来的其它参数删除,然后返回一个错误。 336 | 337 | 对于其它的非 S-表达式类型,我们就直接将其返回。 338 | 339 | ```c 340 | lval* lval_eval_sexpr(lval* v) { 341 | 342 | /* Evaluate Children */ 343 | for (int i = 0; i < v->count; i++) { 344 | v->cell[i] = lval_eval(v->cell[i]); 345 | } 346 | 347 | /* Error Checking */ 348 | for (int i = 0; i < v->count; i++) { 349 | if (v->cell[i]->type == LVAL_ERR) { return lval_take(v, i); } 350 | } 351 | 352 | /* Empty Expression */ 353 | if (v->count == 0) { return v; } 354 | 355 | /* Single Expression */ 356 | if (v->count == 1) { return lval_take(v, 0); } 357 | 358 | /* Ensure First Element is Symbol */ 359 | lval* f = lval_pop(v, 0); 360 | if (f->type != LVAL_SYM) { 361 | lval_del(f); lval_del(v); 362 | return lval_err("S-expression Does not start with symbol!"); 363 | } 364 | 365 | /* Call builtin with operator */ 366 | lval* result = builtin_op(v, f->sym); 367 | lval_del(f); 368 | return result; 369 | } 370 | ``` 371 | 372 | ```c 373 | lval* lval_eval(lval* v) { 374 | /* Evaluate Sexpressions */ 375 | if (v->type == LVAL_SEXPR) { return lval_eval_sexpr(v); } 376 | /* All other lval types remain the same */ 377 | return v; 378 | } 379 | ``` 380 | 381 | 上面的代码中用到了两个我们还没有定义的函数:`lval_pop` 和 `lval_take`。这两个都是用于操作 `lval` 类型的通用型函数,我们在本章和之后的章节中都会用到。 382 | 383 | `lval_pop` 函数将所操作的 S-表达式的第 `i` 个元素取出,并将在其后面的元素向前移动填补空缺,使得这个 S-表达式不再包含这个元素。然后将取出的元素返回。需要注意的是,这个函数并不会将这个 S- 表达式删除。它只是从中取出某个元素,剩下的元素都保持原样。这意味着这两部分最终都需要在某个地方使用 `lval_del` 函数删除。 384 | 385 | `lval_take` 和 `lval_pop` 函数类似,不过它将取出元素之后剩下的列表删除了。它利用了 `lval_pop` 函数并做了一点小小的改变,却使得我们的代码可读性更高了一些。所以,不同于 `lval_pop`,你只需负责使用 `lval_del` 删除取出的元素即可。 386 | 387 | ```c 388 | lval* lval_pop(lval* v, int i) { 389 | /* Find the item at "i" */ 390 | lval* x = v->cell[i]; 391 | 392 | /* Shift memory after the item at "i" over the top */ 393 | memmove(&v->cell[i], &v->cell[i+1], 394 | sizeof(lval*) * (v->count-i-1)); 395 | 396 | /* Decrease the count of items in the list */ 397 | v->count--; 398 | 399 | /* Reallocate the memory used */ 400 | v->cell = realloc(v->cell, sizeof(lval*) * v->count); 401 | return x; 402 | } 403 | ``` 404 | 405 | ```c 406 | lval* lval_take(lval* v, int i) { 407 | lval* x = lval_pop(v, i); 408 | lval_del(v); 409 | return x; 410 | } 411 | ``` 412 | 413 | 我们还需要定义求值函数 `builtin_op`,它和我们在之前章节定义的 `eval_op` 函数类似,改成了接受一个 `lval*` 来代表一系列的参数。该函数应该对参数做更加严格的检查,如果有任何参数不是数字类型的 `lval*`,都应该返回一个错误。 414 | 415 | 首先,它确保所有的输入参数的类型都为数字。然后将第一个数字弹出开始计算。如果后面没有其它的子表达式,并且操作符为减号时,它会对第一个数字进行取反操作。这确保了类似于 (- 5) 这种表达式能够正确工作。 416 | 417 | 如果还有更多的参数,它就不断地从列表中取出,将其和之前的计算结果一起进行相应的数学运算。如果做除法时遇到被除数为零的情况,就将临时变量 x 和 y 以及参数列表删除,并返回一个错误。 418 | 419 | 如果没有错误,参数列表最终会被删除,并返回一个新的表达式。 420 | 421 | ```c 422 | lval* builtin_op(lval* a, char* op) { 423 | 424 | /* Ensure all arguments are numbers */ 425 | for (int i = 0; i < a->count; i++) { 426 | if (a->cell[i]->type != LVAL_NUM) { 427 | lval_del(a); 428 | return lval_err("Cannot operate on non-number!"); 429 | } 430 | } 431 | 432 | /* Pop the first element */ 433 | lval* x = lval_pop(a, 0); 434 | 435 | /* If no arguments and sub then perform unary negation */ 436 | if ((strcmp(op, "-") == 0) && a->count == 0) { 437 | x->num = -x->num; 438 | } 439 | 440 | /* While there are still elements remaining */ 441 | while (a->count > 0) { 442 | 443 | /* Pop the next element */ 444 | lval* y = lval_pop(a, 0); 445 | 446 | if (strcmp(op, "+") == 0) { x->num += y->num; } 447 | if (strcmp(op, "-") == 0) { x->num -= y->num; } 448 | if (strcmp(op, "*") == 0) { x->num *= y->num; } 449 | if (strcmp(op, "/") == 0) { 450 | if (y->num == 0) { 451 | lval_del(x); lval_del(y); 452 | x = lval_err("Division By Zero!"); break; 453 | } 454 | x->num /= y->num; 455 | } 456 | 457 | lval_del(y); 458 | } 459 | 460 | lval_del(a); return x; 461 | } 462 | ``` 463 | 464 | 到此我们的求值函数就完成了。我们只需要再次更新一下 main 函数,在其打印表达式之前,先将输入经由求值函数处理即可。 465 | 466 | ```c 467 | lval* x = lval_eval(lval_read(r.output)); 468 | lval_println(x); 469 | lval_del(x); 470 | ``` 471 | 472 | 现在,你应该可以向上一章一样,正确地对表达式进行求值了。稍作休息,尝试一下新的求值过程。确保能够正常运行,运行结果正确。在下一章,我们将充分利用本章做出的改变,实现一些更加酷炫的特性。 473 | 474 | ```c 475 | lispy> + 1 (* 7 5) 3 476 | 39 477 | lispy> (- 100) 478 | -100 479 | lispy> 480 | () 481 | lispy> / 482 | / 483 | lispy> (/ ()) 484 | Error: Cannot operate on non-number! 485 | lispy> 486 | ``` 487 | 488 | ## 参考 489 | 490 | **s_expressions.c** 491 | 492 | ```c 493 | #include "mpc.h" 494 | 495 | #ifdef _WIN32 496 | 497 | static char buffer[2048]; 498 | 499 | char* readline(char* prompt) { 500 | fputs(prompt, stdout); 501 | fgets(buffer, 2048, stdin); 502 | char* cpy = malloc(strlen(buffer)+1); 503 | strcpy(cpy, buffer); 504 | cpy[strlen(cpy)-1] = '\0'; 505 | return cpy; 506 | } 507 | 508 | void add_history(char* unused) {} 509 | 510 | #else 511 | #include 512 | #include 513 | #endif 514 | 515 | /* Add SYM and SEXPR as possible lval types */ 516 | enum { LVAL_ERR, LVAL_NUM, LVAL_SYM, LVAL_SEXPR }; 517 | 518 | typedef struct lval { 519 | int type; 520 | long num; 521 | /* Error and Symbol types have some string data */ 522 | char* err; 523 | char* sym; 524 | /* Count and Pointer to a list of "lval*"; */ 525 | int count; 526 | struct lval** cell; 527 | } lval; 528 | 529 | /* Construct a pointer to a new Number lval */ 530 | lval* lval_num(long x) { 531 | lval* v = malloc(sizeof(lval)); 532 | v->type = LVAL_NUM; 533 | v->num = x; 534 | return v; 535 | } 536 | 537 | /* Construct a pointer to a new Error lval */ 538 | lval* lval_err(char* m) { 539 | lval* v = malloc(sizeof(lval)); 540 | v->type = LVAL_ERR; 541 | v->err = malloc(strlen(m) + 1); 542 | strcpy(v->err, m); 543 | return v; 544 | } 545 | 546 | /* Construct a pointer to a new Symbol lval */ 547 | lval* lval_sym(char* s) { 548 | lval* v = malloc(sizeof(lval)); 549 | v->type = LVAL_SYM; 550 | v->sym = malloc(strlen(s) + 1); 551 | strcpy(v->sym, s); 552 | return v; 553 | } 554 | 555 | /* A pointer to a new empty Sexpr lval */ 556 | lval* lval_sexpr(void) { 557 | lval* v = malloc(sizeof(lval)); 558 | v->type = LVAL_SEXPR; 559 | v->count = 0; 560 | v->cell = NULL; 561 | return v; 562 | } 563 | 564 | void lval_del(lval* v) { 565 | 566 | switch (v->type) { 567 | /* Do nothing special for number type */ 568 | case LVAL_NUM: break; 569 | 570 | /* For Err or Sym free the string data */ 571 | case LVAL_ERR: free(v->err); break; 572 | case LVAL_SYM: free(v->sym); break; 573 | 574 | /* If Sexpr then delete all elements inside */ 575 | case LVAL_SEXPR: 576 | for (int i = 0; i < v->count; i++) { 577 | lval_del(v->cell[i]); 578 | } 579 | /* Also free the memory allocated to contain the pointers */ 580 | free(v->cell); 581 | break; 582 | } 583 | 584 | /* Free the memory allocated for the "lval" struct itself */ 585 | free(v); 586 | } 587 | 588 | lval* lval_add(lval* v, lval* x) { 589 | v->count++; 590 | v->cell = realloc(v->cell, sizeof(lval*) * v->count); 591 | v->cell[v->count-1] = x; 592 | return v; 593 | } 594 | 595 | lval* lval_pop(lval* v, int i) { 596 | /* Find the item at "i" */ 597 | lval* x = v->cell[i]; 598 | 599 | /* Shift memory after the item at "i" over the top */ 600 | memmove(&v->cell[i], &v->cell[i+1], 601 | sizeof(lval*) * (v->count-i-1)); 602 | 603 | /* Decrease the count of items in the list */ 604 | v->count--; 605 | 606 | /* Reallocate the memory used */ 607 | v->cell = realloc(v->cell, sizeof(lval*) * v->count); 608 | return x; 609 | } 610 | 611 | lval* lval_take(lval* v, int i) { 612 | lval* x = lval_pop(v, i); 613 | lval_del(v); 614 | return x; 615 | } 616 | 617 | void lval_print(lval* v); 618 | 619 | void lval_expr_print(lval* v, char open, char close) { 620 | putchar(open); 621 | for (int i = 0; i < v->count; i++) { 622 | 623 | /* Print Value contained within */ 624 | lval_print(v->cell[i]); 625 | 626 | /* Don't print trailing space if last element */ 627 | if (i != (v->count-1)) { 628 | putchar(' '); 629 | } 630 | } 631 | putchar(close); 632 | } 633 | 634 | void lval_print(lval* v) { 635 | switch (v->type) { 636 | case LVAL_NUM: printf("%li", v->num); break; 637 | case LVAL_ERR: printf("Error: %s", v->err); break; 638 | case LVAL_SYM: printf("%s", v->sym); break; 639 | case LVAL_SEXPR: lval_expr_print(v, '(', ')'); break; 640 | } 641 | } 642 | 643 | void lval_println(lval* v) { lval_print(v); putchar('\n'); } 644 | 645 | lval* builtin_op(lval* a, char* op) { 646 | 647 | /* Ensure all arguments are numbers */ 648 | for (int i = 0; i < a->count; i++) { 649 | if (a->cell[i]->type != LVAL_NUM) { 650 | lval_del(a); 651 | return lval_err("Cannot operate on non-number!"); 652 | } 653 | } 654 | 655 | /* Pop the first element */ 656 | lval* x = lval_pop(a, 0); 657 | 658 | /* If no arguments and sub then perform unary negation */ 659 | if ((strcmp(op, "-") == 0) && a->count == 0) { 660 | x->num = -x->num; 661 | } 662 | 663 | /* While there are still elements remaining */ 664 | while (a->count > 0) { 665 | 666 | /* Pop the next element */ 667 | lval* y = lval_pop(a, 0); 668 | 669 | /* Perform operation */ 670 | if (strcmp(op, "+") == 0) { x->num += y->num; } 671 | if (strcmp(op, "-") == 0) { x->num -= y->num; } 672 | if (strcmp(op, "*") == 0) { x->num *= y->num; } 673 | if (strcmp(op, "/") == 0) { 674 | if (y->num == 0) { 675 | lval_del(x); lval_del(y); 676 | x = lval_err("Division By Zero."); 677 | break; 678 | } 679 | x->num /= y->num; 680 | } 681 | 682 | /* Delete element now finished with */ 683 | lval_del(y); 684 | } 685 | 686 | /* Delete input expression and return result */ 687 | lval_del(a); 688 | return x; 689 | } 690 | 691 | lval* lval_eval(lval* v); 692 | 693 | lval* lval_eval_sexpr(lval* v) { 694 | 695 | /* Evaluate Children */ 696 | for (int i = 0; i < v->count; i++) { 697 | v->cell[i] = lval_eval(v->cell[i]); 698 | } 699 | 700 | /* Error Checking */ 701 | for (int i = 0; i < v->count; i++) { 702 | if (v->cell[i]->type == LVAL_ERR) { return lval_take(v, i); } 703 | } 704 | 705 | /* Empty Expression */ 706 | if (v->count == 0) { return v; } 707 | 708 | /* Single Expression */ 709 | if (v->count == 1) { return lval_take(v, 0); } 710 | 711 | /* Ensure First Element is Symbol */ 712 | lval* f = lval_pop(v, 0); 713 | if (f->type != LVAL_SYM) { 714 | lval_del(f); lval_del(v); 715 | return lval_err("S-expression Does not start with symbol."); 716 | } 717 | 718 | /* Call builtin with operator */ 719 | lval* result = builtin_op(v, f->sym); 720 | lval_del(f); 721 | return result; 722 | } 723 | 724 | lval* lval_eval(lval* v) { 725 | /* Evaluate Sexpressions */ 726 | if (v->type == LVAL_SEXPR) { return lval_eval_sexpr(v); } 727 | /* All other lval types remain the same */ 728 | return v; 729 | } 730 | 731 | lval* lval_read_num(mpc_ast_t* t) { 732 | errno = 0; 733 | long x = strtol(t->contents, NULL, 10); 734 | return errno != ERANGE ? 735 | lval_num(x) : lval_err("invalid number"); 736 | } 737 | 738 | lval* lval_read(mpc_ast_t* t) { 739 | 740 | /* If Symbol or Number return conversion to that type */ 741 | if (strstr(t->tag, "number")) { return lval_read_num(t); } 742 | if (strstr(t->tag, "symbol")) { return lval_sym(t->contents); } 743 | 744 | /* If root (>) or sexpr then create empty list */ 745 | lval* x = NULL; 746 | if (strcmp(t->tag, ">") == 0) { x = lval_sexpr(); } 747 | if (strstr(t->tag, "sexpr")) { x = lval_sexpr(); } 748 | 749 | /* Fill this list with any valid expression contained within */ 750 | for (int i = 0; i < t->children_num; i++) { 751 | if (strcmp(t->children[i]->contents, "(") == 0) { continue; } 752 | if (strcmp(t->children[i]->contents, ")") == 0) { continue; } 753 | if (strcmp(t->children[i]->contents, "}") == 0) { continue; } 754 | if (strcmp(t->children[i]->contents, "{") == 0) { continue; } 755 | if (strcmp(t->children[i]->tag, "regex") == 0) { continue; } 756 | x = lval_add(x, lval_read(t->children[i])); 757 | } 758 | 759 | return x; 760 | } 761 | 762 | int main(int argc, char** argv) { 763 | 764 | mpc_parser_t* Number = mpc_new("number"); 765 | mpc_parser_t* Symbol = mpc_new("symbol"); 766 | mpc_parser_t* Sexpr = mpc_new("sexpr"); 767 | mpc_parser_t* Expr = mpc_new("expr"); 768 | mpc_parser_t* Lispy = mpc_new("lispy"); 769 | 770 | mpca_lang(MPCA_LANG_DEFAULT, 771 | " \ 772 | number : /-?[0-9]+/ ; \ 773 | symbol : '+' | '-' | '*' | '/' ; \ 774 | sexpr : '(' * ')' ; \ 775 | expr : | | ; \ 776 | lispy : /^/ * /$/ ; \ 777 | ", 778 | Number, Symbol, Sexpr, Expr, Lispy); 779 | 780 | puts("Lispy Version 0.0.0.0.5"); 781 | puts("Press Ctrl+c to Exit\n"); 782 | 783 | while (1) { 784 | 785 | char* input = readline("lispy> "); 786 | add_history(input); 787 | 788 | mpc_result_t r; 789 | if (mpc_parse("", input, Lispy, &r)) { 790 | lval* x = lval_eval(lval_read(r.output)); 791 | lval_println(x); 792 | lval_del(x); 793 | mpc_ast_delete(r.output); 794 | } else { 795 | mpc_err_print(r.error); 796 | mpc_err_delete(r.error); 797 | } 798 | 799 | free(input); 800 | 801 | } 802 | 803 | mpc_cleanup(5, Number, Symbol, Sexpr, Expr, Lispy); 804 | 805 | return 0; 806 | } 807 | ``` 808 | 809 | ## 福利 810 | 811 | - 举例说明在我们的程序中哪个变量是在**栈**上的。 812 | - 举例说明在我们的程序中哪个变量是在**堆**上的。 813 | - `strcpy` 函数的作用是什么? 814 | - `realloc` 函数的作用是什么? 815 | - `memmove` 函数的作用是什么? 816 | - `memmove` 于 `memcpy` 有何不同? 817 | - 扩展解析和求值部分使其支持求余操作符 `%`。 818 | - 扩展解析和求值部分使其支持浮点数(`double`)。 -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [第零零章 • 关于](README.md) 4 | * [第零一章 • 介绍](Introduction.md) 5 | * [第零二章 • 安装](Installation.md) 6 | * [第零三章 • 基础](Basics.md) 7 | * [第零四章 • 交互](Interactive.md) 8 | * [第零五章 • 编程语言](Languages.md) 9 | * [第零六章 • 语法分析](Parsing.md) 10 | * [第零七章 • 计算](Evaluation.md) 11 | * [第零八章 • 错误处理](Error Handling.md) 12 | * [第零九章 • S-表达式](S-Expressions.md) 13 | * [第零十章 • Q-表达式](Q-Expressions.md) 14 | * [第十一章 • 变量](Variables.md) 15 | * [第十二章 • 函数](Functions.md) 16 | * [第十三章 • 条件分支](Conditionals.md) 17 | * [第十四章 • 字符串](Strings.md) 18 | * [第十五章 • 标准库](Standard Library.md) 19 | * [第十六章 • 彩蛋](Bonus Projects.md) 20 | * [第十七章 • 附录](Appendix.md) 21 | 22 | -------------------------------------------------------------------------------- /Standard Library.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksco/BuildYourOwnLispCn/b3d3ebd3187a85909d137a7a4d88612912cd4c02/Standard Library.md -------------------------------------------------------------------------------- /Strings.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksco/BuildYourOwnLispCn/b3d3ebd3187a85909d137a7a4d88612912cd4c02/Strings.md -------------------------------------------------------------------------------- /Variables.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksco/BuildYourOwnLispCn/b3d3ebd3187a85909d137a7a4d88612912cd4c02/Variables.md --------------------------------------------------------------------------------