目录

前文列表

用 C 语言开发一门编程语言 — 交互式解释器l

用 C 语言开发一门编程语言 — 跨平台的可移植性

编程语言的本质

在 19 世纪 50 年代,语言学家 Noam Chomsky 定义了一系列关于语言的重要理论。这些理论支撑了我们今天对于语言结构的基本理解。其中重要的一条结论就是:自然语言都是建立在递归和重复的子结构之上的。Chomsky 提出的理论是非常重要的。它意味着,虽然一门语言可以表达无限的内容,我们仍然可以使用有限的规则去解析所有用该门语言写就的东西。这些有限的规则就叫语法(grammar)。

当我们学习一门自然语言的时候,我们往往从语法开始。当我们学习一门编程语言的时候也一样,当我们尝试开发一门编程语言的时候亦如此,首先要考虑的就是语言的语法、及其语义。

词法分析

大多数编程语言开发的第一步是词法分析或分词。通常使用 “Lex” 或 “Tokenizer” 来进行描述,表示将一大堆文本分解成多个符号。

词法分析器将包含源码的文件作为输入字符串,输出包含标记符号的列表。那么,在编译的后半阶段将不再参考这些字符串源代码,所以词法分析器必须产生所有后面各阶段需要的信息。之所以会有这样相对严格的格式设计,是因为这个阶段词法分析器可以做一些工作,比如移除注释或检测标识符或数字等。如果你将这些逻辑规则放在词法分析器里,那么在构造语言的其它部分时就不必再考虑这些规则了,而且你可以方便地在同一个地方集中修改这些语法规则。

语法分析

编译的第二阶段就是语法分析。语法分析器把标识符列表解析为一个带结点的树。用于存储这种数据的树称为 AST(抽象语法树)。

为了定义一门编程语言的语法,首先需要能够正确解析用户按照语法规则编写的程序。为此,需要编程语言程序就需要一个语法解析器,用来判断用户的输入是否合法,并产生解析后的内部表示。内部表示是一种计算机更容易理解的表示形式,有了它,我们后面的解析、求值等工作会变得更加的简单可行。

使用 MPC 解析器组合库

MPC(Micro Parser Combinators)是一个用于 C 的轻量且强大的解析器组合库。你可以使用这个库为任何语言编写语法解析器。编写语法解析器的方法有很多,使用解析器组合库的好处就在于,它极大地简化了原本枯燥无聊的工作,你只需要关注编写高层的抽象语法规则就可以了。

注:MPC 的开发者就是《Build Your Own Lisp》的原作者。

MPC 可用于:

  • 解析现有的,或开发新的编程语言
  • 解析现有的,或开发新的数据格式

MPC 的特性:

  • 正则表达式分析器生成器
  • 语法分析器生成器
  • 易于集成到 C 语言项目(以一个源文件的形式存在)
  • 自动生成错误消息
  • Type-Generic(泛式类型)
  • Predictive, Recursive Descent

安装

在我们正式编写这个语法解析器之前,首先需要安装 MPC 库。MPC 库的安装非常简单,只需要将源码下载,把源文件 Copy 到我们的 C 语言项目中,然后在项目中包含 mpc 的头文件并链接 MPC 库即可。

下载

$ git clone https://github.com/orangeduck/mpc.git

Copy

$  ll
总用量 140
-rw-r--r-- 1 root root 111731 4月 7 18:12 mpc.c
-rw-r--r-- 1 root root 11194 4月 7 18:12 mpc.h
-rwxr-xr-x 1 root root 8632 4月 7 18:08 parsing
-rw-r--r-- 1 root root 1203 4月 7 18:11 parsing.c

引入到 parsing.c

#include "mpc.h"

编译

gcc -std=c99 -Wall parsing.c mpc.c -lreadline -lm -o parsing
  • -lm:链接数学库。

快速入门

下面我们以编写一个 Doge(the language of Shiba Inu,柴犬语)语言的语法解析器为例,来快速熟悉 MPC 的用法。

首先解构一下 Doge 语言的语法结构:

  • Adjective(形容词):wow、many、so、such。
  • Noun(名词):lisp、language、c、book、build。
  • Phrase(短语):由 Adjective + Noun 组成。
  • Doge(柴犬语):由若干个 Phrase 组成。

再来解释一下 Doge 语言的语法描述:

  • Phrase 由 Adjective 和 Noun 组成。
  • Doge 由任意个 Phrase 组成。

最后,我们就可以尝试使用 MPC 来定义 Doge 语言的语法解析器了:

  • Step 1. 使用 MPC 定义 Adjective 和 Noun,为此我们创建两个解析器。其中,mpc_or 函数会返回一个解析器,该解析器表示 “取其一”,因为我们需要从 Adjective 和 Noun 中 “各取其一” 来组成 Phrase,所以分别定义了两个解析器。
/* Build a parser 'Adjective' to recognize descriptions */
mpc_parser_t *Adjective = mpc_or(4,
mpc_sym("wow"), mpc_sym("many"),
mpc_sym("so"), mpc_sym("such")
); /* Build a parser 'Noun' to recognize things */
mpc_parser_t *Noun = mpc_or(5,
mpc_sym("lisp"), mpc_sym("language"),
mpc_sym("book"),mpc_sym("build"),
mpc_sym("c")
);
  • Step 2. 使用已经定义好的 Adjective 、 Noun 解析器来定义 Phrase 解析器。mpc_and 函数会返回一个解析器,该解析器只接受各 “子句” 按照顺序出现的语句。所以我们将先前定义的 Adjective 和 Noun 传递给它,表示:形容词后面紧跟着名词组成的短语。mpcf_strfold 和 free 指定了各个语句的组织(Fold)及删除(Free)方式。在 mpcf_strfold 和 free 函数的帮助下,我们不用担心什么时候加入和丢弃输入,它们将自动帮助我们完成。
mpc_parser_t *Phrase = mpc_and(2, mpcf_strfold, Adjective, Noun, free);
  • Step 3. 使用 Phrase 解析器来最终定义 Doge 语言的解析器,Doge 是由若干个 Phrase 组成的,mpc_many 函数表达的正是这种逻辑关系。
mpc_parser_t *Doge = mpc_many(mpcf_strfold, Phrase);

上述语句表明 Doge 可以接受任意多条语句。这也意味着 Doge 语言是无穷的。下面列出了一些符合 Doge 语法的例子:

/* 一条 Doge 语句由若干个 Phrase 组成,一个 Phrase 由一个 Adjective + 一个 Noun 构成。 */
"wow book such language many lisp"
"so c such build such language"
"many build wow c"
""
"wow lisp wow c many language"
"so c"

通过上述步骤,我们简单的定义了一门 Doge 语言的描述自己实现了一门 Doge 语言的语法解析器。还可以继续使用 mpc 提供的其他函数,一步一步地编写能解析更加复杂的语法的解析器。

但是很显然的,上述的代码实现方式并不友好,随着语法的复杂度的增加,代码的可读性也会越来越差。所以 mpc 还提供了一系列的函数来帮助用户更加简单地完成常见的任务,使用这些函数能够更好更快地构建复杂语言的解析器,并能够提供更加精细地控制。具体的文档说明可以参见项目主页*(https://github.com/orangeduck/mpc)*。

下面,我们使用 MPC 提供的另一种更加简易的代码实现方式来编写 Doge 语法解析器:将整个语言的语法规则写在一个长字符串中,而不是使用啰嗦难懂的 C 语句。我们也不再需要关心如何使用 mpcf_strfold 或是 free 参数组织或删除各个语句。所有的这些工作都是都是自动完成的。

mpc_parser_t* Adjective = mpc_new("adjective");
mpc_parser_t* Noun = mpc_new("noun");
mpc_parser_t* Phrase = mpc_new("phrase");
mpc_parser_t* Doge = mpc_new("doge"); mpca_lang(MPCA_LANG_DEFAULT,
" \
adjective : \"wow\" | \"many\" \
| \"so\" | \"such\"; \
noun : \"lisp\" | \"language\" \
| \"book\" | \"build\" | \"c\"; \
phrase : <adjective> <noun>; \
doge : <phrase>*; \
",
Adjective, Noun, Phrase, Doge); /* Do some parsing here... */ mpc_cleanup(4, Adjective, Noun, Phrase, Doge);
  1. 使用 mpc_new 函数定义解析器的名字。
  2. 使用 mpca_lang 函数定义这些解析器的内涵和解析器之间的逻辑关系,从而最终构成一门语言的语法规则。

mpca_lang 函数的第一个参数是操作标记,在这里我们使用默认选项 MPCA_LANG_DEFAULT。第二个参数是 C 语言的一个长字符串。这个字符串中定义了具体的语法规则。每个规则分为两部分,用冒号 : 隔开,使用 ; 表示规则结束:

  • 冒号左边是语法规则的名字,e.g. adjective、noun、phrase、doge。
  • 右边是语法规则的定义,e.g. adjective:wow、many、so、such。

mpca_lang 函数就是对 mpc_many、mpc_and 、 mpc_or 这些函数的封装,自动地完成这些函数的工作,让解析器定义的代码变得干净利落,不拖泥带水。

定义语法规则的一些特殊符号的作用如下:

实现波兰表达式的语法解析

波兰表达式

就如我们常见的算数表达式 1+1 = 2。波兰表达式也是一种数学标记语言,它的特点是运算符会在操作数的前面。我们考虑将波兰表达式作为 Lisp 编程语言的数学运算部分。



在编写这种数据标记语言的语法规则之前,我们还是先对其进行语法解构以及语法描述:我们观察到,波兰表达式总是以操作符开头,后面跟着操作数或其他包裹在圆括号中的表达式。也就是说,波兰表达式是由:程序(Program)是由一个操作符(Operator)加上一个或多个表达式(Expression)组成的。而每个表达式又可以是一个数字或者是包裹在圆括号中的一个操作符加上一个或多个表达式。

所以我们对波兰表达式进行了如下解构和描述:

正则表达式

有了语法规则的描述之后,我们还需要对针对语法的输入进行约束(e.g. 如何表达开始和结束输入、如何规定可选字符和字符范围等),因为一个任意的输入中,很可能包含了一些解析器还没定义清楚的结构。我们考虑使用正则表达式(Regular Expression)来实现这一目的。

正则表达式适合定义一些小型的语法规则,例如单词或是数字等。正则表达式不支持复杂的规则,但它清晰且精确地界定了输入是否符合规则。在 mpc 中,我们需要将正则表达式包裹在一对 / 中。例如,Number 可以用 /-?[0-9]+/ 来表示。

代码实现

根据上面的分析,我们可以定义波兰表达式最终的语法规则:

#include <stdio.h>
#include <stdlib.h>
#include "mpc.h" #ifdef _WIN32
#include <string.h> static char buffer[2048]; char *readline(char *prompt) {
fputs(prompt, stdout);
fgets(buffer, 2048, stdin); char *cpy = malloc(strlen(buffer) + 1); strcpy(cpy, buffer);
cpy[strlen(cpy) - 1] = '\0'; return cpy;
} void add_history(char *unused) {} #else #ifdef __linux__
#include <readline/readline.h>
#include <readline/history.h>
#endif #ifdef __MACH__
#include <readline/readline.h>
#endif #endif int main(int argc, char *argv[]) { /* Create Some Parsers */
mpc_parser_t *Number = mpc_new("number");
mpc_parser_t *Operator = mpc_new("operator");
mpc_parser_t *Expr = mpc_new("expr");
mpc_parser_t *Lispy = mpc_new("lispy"); /* Define them with the following Language */
mpca_lang(MPCA_LANG_DEFAULT,
" \
number : /-?[0-9]+/ ; \
operator : '+' | '-' | '*' | '/' ; \
expr : <number> | '(' <operator> <expr>+ ')' ; \
lispy : /^/ <operator> <expr>+ /$/ ; \
",
Number, Operator, Expr, Lispy); puts("Lispy Version 0.1");
puts("Press Ctrl+c to Exit\n"); while(1) {
char *input = NULL; input = readline("lispy> ");
add_history(input); /* Attempt to parse the user input.
* 解析用户的每一条输入。
* 调用了 mpc_parse 函数,并将 Lispy 解析器和用户输入 input 作为参数。
* 它将解析的结果保存到 &r 中,如果解析成功,函数返回值为 1,失败为 0。
*/
mpc_result_t r;
if (mpc_parse("<stdin>", input, Lispy, &r)) {
/* On success print and delete the AST.
* 解析成功时会产生一个内部结构,并保存到 r 的 output 字段中。
* 我们可以使用 mpc_ast_print 将这个结构打印出来,使用 mpc_ast_delete 将其删除。
*/
mpc_ast_print(r.output);
mpc_ast_delete(r.output);
} else {
/* Otherwise print and delete the Error.
* 解析失败时则会将错误信息保存在 r 的 error 字段中。
* 我们可以使用 mpc_err_print 将这个结构打印出来,使用 mpc_err_delete 将其删除。
*/
mpc_err_print(r.error);
mpc_err_delete(r.error);
} free(input); } /* Undefine and delete our parsers */
mpc_cleanup(4, Number, Operator, Expr, Lispy); return 0;
}

编译:

gcc -std=c99 -Wall parsing.c mpc.c -lreadline -lm -o parsing

运行,尝试解析一条波兰表达式 + 5 (* 2 2)

$ ./parsing
Lispy Version 0.1
Press Ctrl+c to Exit lispy> + 5 (* 2 2)
>
regex
operator|char:1:1 '+'
expr|number|regex:1:3 '5'
expr|>
char:1:5 '('
operator|char:1:6 '*'
expr|number|regex:1:8 '2'
expr|number|regex:1:10 '2'
char:1:11 ')'
regex
lispy> hello
<stdin>:1:1: error: expected '+', '-', '*' or '/' at 'h'
lispy> / 1dog
<stdin>:1:4: error: expected one of '0123456789', '-', one or more of one of '0123456789', '(', newline or end of input at 'd'
lispy>
<stdin>:1:1: error: expected '+', '-', '*' or '/' at end of input
lispy> ^C

可以看见,上述的正常输出就是一个 AST(Abstract Syntax Tree,抽象语法树),它用来表示用户输入的表达式的结构。操作数(Number)和操作符(Operator)等需要被处理的实际数据都位于叶子节点上。而非叶子节点上则包含了遍历和求值的信息。

用 C 语言开发一门编程语言 — 语法解析器的更多相关文章

  1. Anrlr4 生成C++版本的语法解析器

    一. 写在前面 我最早是在2005年,首次在实际开发中实现语法解析器,当时调研了Yacc&Lex,觉得风格不是太好,关键当时yacc对多线程也支持的不太好,接着就又学习了Bison&F ...

  2. 在.NET Core中使用Irony实现自己的查询语言语法解析器

    在之前<在ASP.NET Core中使用Apworks快速开发数据服务>一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语 ...

  3. Boost学习之语法解析器--Spirit

    Boost.Spirit能使我们轻松地编写出一个简单脚本的语法解析器,它巧妙利用了元编程并重载了大量的C++操作符使得我们能够在C++里直接使用类似EBNF的语法构造出一个完整的语法解析器(同时也把C ...

  4. 使用 java 实现一个简单的 markdown 语法解析器

    1. 什么是 markdown Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者,撰稿者广泛使用.看到这里请不要被「标记」.「语言」所迷惑,Markdown 的 ...

  5. C# 语言的两个html解析器

    基于C# 语言的两个html解析器   基于C# 语言的两个html解析器 1)Html Agility Pack http://nsoup.codeplex.com/ 代码段示例: HtmlDocu ...

  6. 基于C# 语言的两个html解析器

    基于C# 语言的两个html解析器 1)Html Agility Pack http://nsoup.codeplex.com/ 代码段示例: HtmlDocument doc = new HtmlD ...

  7. 用java实现编译器-算术表达式及其语法解析器的实现

    大家在参考本节时,请先阅读以下博文,进行预热: http://blog.csdn.net/tyler_download/article/details/50708807 本节代码下载地址: http: ...

  8. 手写token解析器、语法解析器、LLVM IR生成器(GO语言)

    最近开始尝试用go写点东西,正好在看LLVM的资料,就写了点相关的内容 - 前端解析器+中间代码生成(本地代码的汇编.执行则靠LLVM工具链完成) https://github.com/daibinh ...

  9. 邵国际: C 语言对象化设计实例 —— 命令解析器

    本文系转载,著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 作者: 邵国际 来源: 微信公众号linux阅码场(id: linuxdev) 内容简介 单片机工程师常常疑惑为什么 ...

  10. 语法解析器续:case..when..语法解析计算

    之前写过一篇博客,是关于如何解析类似sql之类的解析器实现参考:https://www.cnblogs.com/yougewe/p/13774289.html 之前的解析器,更多的是是做语言的翻译转换 ...

随机推荐

  1. #dp,矩阵乘法#洛谷 5371 [SNOI2019]纸牌

    题目 一副纸牌有 \(n\) 种,每种有 \(m\) 张, 现在有 \(k\) 个限制条件形如第 \(k_i\) 种牌至少选 \(a_i\) 张, 一个三元组合法当且仅当其为 \((i,i+1,i+2 ...

  2. #zkw线段树#洛谷 3792 由乃与大母神原型和偶像崇拜

    题目 给你一个长为 \(n\) 的序列 \(a\) 每次两个操作: 修改 \(x\) 位置的值为 \(y\) 查询区间 \([l,r]\) 是否可以重排为值域上连续的一段 分析 直接维护区间最大值和最 ...

  3. MySQL 主从 AUTO_INCREMENT 不一致问题分析

    作者:vivo 互联网数据库团队 - Wei Haodong 本文介绍了 MySQL5.7 中常见的replace into 操作造成的主从auto_increment不一致现象,一旦触发了主从切换, ...

  4. 开放原子开源基金会OpenHarmony工作委员会主席侯培新寄语OpenAtom OpenHarmony分论坛

    2022开放原子全球开源峰会 OpenAtom OpenHarmony分论坛 万物互联,使能千行百业 7月27日 14:00  与您相约 OpenHarmony 工作委员会主席侯培新 寄语 OpenA ...

  5. ASP.NET 部署常见问题及解决方案

    ASP.NET 部署部署过程中常见问题及解决方案 Could not load file or assembly 'XXXXX' or one of its dependencies. Access ...

  6. Python 爬虫进阶五之多线程的用法

    Python 爬虫进阶五之多线程的用法 作者 崔庆才   发表于 2016-11-03   分类于 Python   阅读次数: 60553   本文字数: 7.5k   阅读时长 ≈ 7 分钟 前言 ...

  7. CentOS 利用pam控制ssh用户的登录及SSH安全配置

    CentOS 利用pam控制ssh用户的登录 有关pam的使用,请找相关的文档.下面只说两个简单的例子. 首先在/etc/pam.d/sshd加入一句: account    required     ...

  8. vue3 快速入门系列 —— 组件通信

    vue3 快速入门系列 - 组件通信 组件通信在开发中非常重要,通信就是你给我一点东西,我给你一点东西. 本篇将分析 vue3 中组件间的通信方式. Tip:下文提到的绝大多数通信方式在 vue2 中 ...

  9. JavaScript中数值小知识

    1. 数值10.0 这种类似的会被去掉数值后的0 之所以这样是因为,整数的存储空间占用比浮点数小,当一个数值不是真浮点数(即10.0这种格式),会被转换为整数10,如果业务中有一些需求需要进行数值位数 ...

  10. ORA-02303: cannot drop or replace a type with type or table dependents,即无法使用类型或表的相关性来删除或取代一个类型

    ORA-02303: cannot drop or replace a type with type or table dependents,即无法使用类型或表的相关性来删除或取代一个类型 在修改一个 ...