本文使用 C 语言+Melon 库实现一个简易脚本语言解析器。
为了便于阅读和理解,解析器不会生成抽象语法树结构,只是对脚本文件的语法进行解析。
Melon 是一个 C 语言库,其中提供了一个用宏封装好的脚本解释器相关的套件,如词法分析器和语法解析器生成器。使用非常简单,最简单的使用仅需非常简短的代码即可。
安装如下:
$ git clone https://github.com/Water-Melon/Melon.git
$ cd Melon
$ ./configure [--prefix=....]
$ make && make install
下面直接上代码,这里我对代码进行了完全注释。
//test.c
#include <stdio.h>
#include <string.h>
#include "mln_core.h"
#include "mln_log.h"
#include "mln_lex.h"
#include "mln_alloc.h"
#include "mln_parser_generator.h"
//声明语法解析器生成器,函数作用域为 static ,函数与结构体名称前缀为 test ,词素前缀为 TEST 。本例没有自定义词素。
MLN_DECLARE_PARSER_GENERATOR(static, test, TEST);
//定义语法解析器生成器,函数作用域为 static ,函数与结构体名称前缀为 test ,词素前缀为 TEST 。本例没有自定义词素。
MLN_DEFINE_PARSER_GENERATOR(static, test, TEST);
//产生式表,产生式的原理与代数非常相似,就是将同名的部分代入展开。start 为所有语法的启始,xxx_TK_EOF 表示语言读取完毕处
//本例中 stm 表示语句( statement ),exp 表示表达式( expression ),addsub 表示加减法表达式。
//TEST_TK_SEMIC 为分号(;),TEST_TK_ID 为变量名(以下划线字母起始,后续自符有字母数字下划线组成),TEST_TK_DEC 为整数。
//这里使用了词法分析器的默认词素切分规则生成的词素,开发者可根据自己需求对关键字、特殊运算符进行扩展。
//
//每一个产生式结构的第二个参数为一个处理函数,该函数会在本产生式冒号(:)右侧所有词素(确切的叫终结符与非终结符)都获取到时被调用。
//我们可以在这些回调函数中生成抽象语法树结构。
static mln_production_t prod_tbl[] = {
{"start: stm TEST_TK_EOF", NULL},
{"stm: exp TEST_TK_SEMIC stm", NULL},
{"stm: ", NULL},
{"exp: TEST_TK_ID addsub", NULL},
{"exp: TEST_TK_DEC addsub", NULL},
{"addsub: TEST_TK_PLUS exp", NULL},
{"addsub: TEST_TK_SUB exp", NULL},
{"addsub: ", NULL},
};
int main(int argc, char *argv[])
{
struct mln_core_attr cattr;
mln_lex_t *lex = NULL;
struct mln_lex_attr lattr;
mln_alloc_t *pool;
mln_string_t path;
struct mln_parse_attr pattr;
mln_u8ptr_t ptr, ast;
mln_lex_hooks_t hooks;
//框架初始化
cattr.argc = argc;
cattr.argv = argv;
cattr.global_init = NULL;
cattr.worker_process = NULL;
if (mln_core_init(&cattr) < 0) {
fprintf(stderr, "Melon init failed.\n");
return -1;
}
//设置自定义语言文本文件路径
mln_string_set(&path, argv[1]);
//创建内存池,用于语法分析过程中使用,使用后可进行释放。这里需要注意,生成的抽象语法树结构尽量不要使用该内存池,
//开发者可能会习惯性按本示例一样在解析后进行释放。则在后续处理抽象语法树时就会发生越界访问。
if ((pool = mln_alloc_init()) == NULL) {
mln_log(error, "init memory pool failed.\n");
return -1;
}
//设置词法分析器内存池
lattr.pool = pool;
//本例没有自定义关键字
lattr.keywords = NULL;
//本例没有对运算符进行扩展
memset(&hooks, 0, sizeof(hooks));
lattr.hooks = &hooks;
//启用预编译机制,启用后,自定义语言中可使用#include 、#def 、#endif 等预编译宏
lattr.preprocess = 1;
//设置为文件路径名类型。待解析内容可以直接给出字符串内容,也可以是文本路径,可参考词法分析器中的定义。
lattr.type = M_INPUT_T_FILE;
//若 type 为 M_INPUT_T_FILE ,则 data 为文件路径,否则为自定义语言字符串。
lattr.data = &path;
//初始化词法分析器
mln_lex_init_with_hooks(test, lex, &lattr);
if (lex == NULL) {
mln_log(error, "init lexer failed.\n");
return -1;
}
//生成状态转换表
ptr = test_parser_generate(prod_tbl, sizeof(prod_tbl)/sizeof(mln_production_t));
if (ptr == NULL) {
mln_log(error, "generate state shift table failed.\n");
return -1;
}
//设置语法解析器内存池
pattr.pool = pool;
//设置产生式
pattr.prod_tbl = prod_tbl;
//设置词法解析器,待解析的语言由词法分析器拆解后交由本解析器处理
pattr.lex = lex;
//设置状态转换表
pattr.pg_data = ptr;
//本例没有自定义数据
pattr.udata = NULL;
//执行解析
ast = test_parse(&pattr);
//...对自定义的抽象语法树结构进行处理,本例中没有定义相关结构,因此会保持为 NULL
//销毁词法分析器
mln_lex_destroy(lex);
//释放内存池
mln_alloc_destroy(pool);
return 0;
}
下面对程序进行编译
$ cc -o test test,c -I /path/to/melon/include -L /path/to/melon/lib -lmelon -lpthread
然后我们编辑一个文本a.test
,其中包含我们的脚本语言:
a + 1;
1 + 1;
_b + a;
然后用我们的脚本解释器来对脚本文件进行解析:
$ ./test a.test
这时可以看到什么都没有输出,这代表语法通过了检查。
下面我们对a.test
进行修改,故意将其改为违反语法规则的文本:
a * 1;
b + a;
c - b;
在此运行程序去解析脚本文件,可得到如下输出:
a.test:1: Parse Error: Illegal token nearby '*'.
这个例子只是简单的展示了一个可以解析语法规则的解析器,对该内容感兴趣的读者可以自行对其扩展实现更多语法和抽象语法树等内容。
感谢阅读!
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.