目录
前言
通过开发一门类 Lisp 的编程语言来理解编程语言的设计思想,本实践来自著名的《Build Your Own Lisp》。
环境
- 操作系统:CentOS7
- 编辑器:VIM
- C 语言标准:C99
编译型 vs 解释型
语言主要有两种类型:编译型和解释型。技术上,任何语言都可以被编译或解释,但是一种或另一种语言通常对于特定语言更有意义。一般来说,解释往往更加灵活,而编译往往具有更高的性能。
当我们希望开发一种解释型语言,那么建议使用编译语言(e.g. C/C ++ 或 Swift)进行开发,否则我们开发的解释型语言的性能损失以及其自身的解释器将会更加复杂。
实现交互式解释器
交互式解释器,这种系统也被叫做 REPL(Read-Evaluate-Print Loop,读取-求值-输出-循环),这种技术被广泛地应用在各种编程语言的解释器中,例如 Python 的 Shell。我们称这种模式为交互提示。
在编写一个完整的 REPL 之前,我们先实现一个简单的程序:读取用户的输入,在程序内部进行处理,然后返回一些信息给用户。
- 使用一个永循环等待用户输入并打印信息。
- 使用 stdio.h 中的 fgets 函数获取用户输入的内容,这个函数可以一直读取直到遇到换行符为止。
- 声明一个固定大小的数组缓冲区来存储用户的输入。
- 一旦获取到用户输入的字符串,就可以使用 printf 将它打印到命令行中。
创建一个 parsing.c 源文件,意为解释器:
#include <stdio.h>
/* Declare a buffer for user input of size 2048.
* 定义了一个拥有 2048 个字符长度的全局数组。这个数组中存储的数据可以在程序的任何地方获取到。
* 我们会把用户在命令中输入的语句保存到这里面来。
* static 关键字标明这个数组仅在本文件中可见。
*/
static char input[2048]; // 2Kb
int main(int argc, char *argv[]) {
/* 把一个字符串写入到标准输出 stdout,直到空字符,但不包括空字符。换行符会被追加到输出中。 */
puts("Lispy Version 0.1");
puts("Press Ctrl+c to Exit
");
/* In a never ending loop */
while(1) {
/* Output our prompt.
* 把字符串写入到指定的 stream 中,但不包括空字符。
* 与 puts 函数区别是 fputs 不会在末尾自动加换行符。
*/
fputs("lispy> ", stdout);
/* Read a line of user input of maximum size 2048.
* 使用 fgets 函数来获取用户在命令行中输入的字符串。
* 这两个函数都需要指定写入或读取的文件。在这里,我们使用 stdin 和 stdout 作为输入和输出。
* 这两个变量都是在 <stdio.h> 中定义的,用来表示向命令行进行输入和输出。
* 当我们把 stdin 传给 fgets 后,它就会等待用户输入一串字符,并按下回车键。
* 如果得到了字串,就会把字串连同换行符存放到 input 数组中。
* 为了不让获取到的数据太大数组装不下,我们还要指定一下可以获取的最大长度为 2048。
*/
fgets(input, 2048, stdin);
/* Echo input back to user */
printf("You're a %s", input);
}
return 0;
}
运行:
$ ./parsing
Lispy Version 0.1
Press Ctrl+c to Exit
lispy> fanguiju
You're a fanguiju
lispy> ^C
使用 GNU Readline 函数库
上述完成了最基本的交互式功能(输入、输出),如果你用的是 Mac 或 Linux,当你用左右箭头键编辑在程序中的输入时,你会遇到一个奇怪的问题:使用箭头键不会前后移动输入的光标,而是会产生像 ^[[D
或 ^[[C
这种奇怪的字符。如下所示。
Lispy Version 0.0.0.0.3
Press Ctrl+c to Exit
lispy> hel^[[D^[[C
注:在 Windows 上则不会有这个现象。
所以,我们还需要完成 Shell 具有的一些特性,例如:使用左右箭头对指令行进行编辑、使用上下箭头来获取历史输入。这里,我们使用 GNU Readline 库来进行改造,把 fputs 和 fgets 替换为这个库提供的相同功能的函数。
安装 GNU Readline 函数库:
yum install readline-devel -y
查看:
$ ll /usr/include | grep readline
drwxr-xr-x 2 root root 143 4月 7 18:06 readline
GNU Readline 函数库提供的两个函数:readline 和 add_history。
- readline 函数:和 fgets 一样,从命令行读取一行输入,并且允许用户使用左右箭头对指令行进行编辑。
- add_history 函数:可以纪录下我们之前输入过的命令,并运行用户使用上下箭头来获取历史输入。
#include <stdio.h>
#include <stdlib.h>
#include <readline/readline.h>
#include <readline/history.h>
int main(int argc, char *argv[]) {
puts("Lispy Version 0.1");
puts("Press Ctrl+c to Exit
");
while(1) {
char *input = NULL;
/* Output our prompt and get input.
* 使用 readline 读取用户输入。
* 与 fgets 不同的是,readline 并不在结尾添加换行符。
* 所以我们在 printf 函数中添加了一个换行符。
*/
input = readline("lispy> ");
/* Add input to history.
* 使用 add_history 将该输入添加到历史纪录当中。
*/
add_history(input);
printf("You're a %s
", input);
/* Free retrieved input.
* 还需要使用 free 函数手动释放 readline 函数返回给我们的缓冲区 input。
* 因为 readline 不同于 fgets 函数,后者使用已经存在的空间,而前者会申请一块新的内存,所以需要手动释放。
*/
free(input);
}
return 0;
}
编译:
gcc -std=c99 -Wall parsing.c -o parsing -lreadline
-l<lib_name>
:指定程序要链接的库。- -std:指定 C 语言标准。
运行:
$ ./parsing
Lispy Version 0.1
Press Ctrl+c to Exit
lispy> fanguiju
You're a fanguiju
lispy> fanguiju # 使用上箭头获取历史输入记录
You're a fanguiju
lispy>