用 C 语言开发一门编程语言 — 语法解析器

不问归期 提交于 2020-04-09 04:08:22

目录

前文列表

用 C 语言开发一门编程语言 — 交互式 Shell
用 C 语言开发一门编程语言 — 跨平台

编程语言的本质

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

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

实现语法解析器

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

使用 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 组成。

下面我们尝试使用 MPC 来定义 Doge 语言。

  • Step 1. 使用 MPC 定义 Adjective 和 Noun,为此我们创建两个解析器:
/* 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")
);

其中,mpc_or 函数会返回一个解析器,该解析器表示 “取其一”,因为我们需要从 Adjective 和 Noun 中 “各取其一” 来组成 Phrase,所以分别定义了两个解析器。

  • Step 2. 使用已经定义好的 Adjective 、 Noun 解析器来定义 Phrase 解析器:
mpc_parser_t *Phrase = mpc_and(2, mpcf_strfold, Adjective, Noun, free);

mpc_and 函数会返回一个解析器,该解析器只接受各 “子句” 按照顺序出现的语句。所以我们将先前定义的 Adjective 和 Noun 传递给它,表示:形容词后面紧跟着名词组成的短语。mpcf_strfold 和 free 指定了各个语句的组织(Fold)及删除(Free)方式。在 mpcf_strfold 和 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 语言。还可以继续使用 mpc 提供的其他函数,一步一步地编写能解析更加复杂的语法的解析器。但很显然,这种代码实现方式并不友好,随着语法的复杂度的增加,代码的可读性也会越来越差。

所以 mpc 还提供了一系列的函数来帮助用户更加简单地完成常见的任务,使用这些函数能够更好更快地构建复杂语言的解析器,并能够提供更加精细地控制。具体的文档说明可以参见项目主页(https://github.com/orangeduck/mpc)。

下面,我们使用 MPC 提供的另一种更加贴近自然的代码实现方式来编写语法规则:将整个语言的语法规则写在一个长字符串中,而不是使用啰嗦难懂的 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. 形容词:wow、many、so、such。

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

定义语法规则的一些特殊符号的作用如下:
在这里插入图片描述

波兰表达式

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

在这里插入图片描述
在编写这种数据标记语言的语法规则之前,我们可以先用白话文来尝试描述它,而后再将其公式化。

我们观察到,波兰表达式总是以操作符开头,后面跟着操作数或其他的包裹在圆括号中的表达式。也就是说:程序(Program)是由一个操作符(Operator)加上一个或多个表达式(Expression)组成的。而每个表达式又可以是一个数字或者是包裹在圆括号中的一个操作符加上一个或多个表达式。

在这里插入图片描述

正则表达式

在定义了数学运算的语法规则之后,还需要对语法输入进行约束(如何表达开始和结束输入、可选字符、字符范围),因为其中可能包含了一些解析器还没定义清楚的结构,我们考虑使用正则表达式(Regular Expression)来实现这一目的。

正则表达式适合定义一些小型的语法规则,例如单词或是数字等。正则表达式不支持复杂的规则,但它清晰且精确地界定了输入是否符合规则。

在这里插入图片描述

在 mpc 中,我们需要将正则表达式包裹在一对 / 中。例如,Number 可以用 /-?[0-9]+/ 来表示。

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

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


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!