Context-free Grammar的编译器设计和实现
已重写的文章在(编译原理(前端)的算法和实现)(2023年6月24日)
我在准备重写本文的内容,本文不必再看。(2023年3月25日)
(注:可以下载本文的PDF版,我也上传到了百度文库,大家可以自己找一下。)
Context-free Grammar的编译器设计和实现
摘要
本文要做的工作如下:
l 手动写Expression Grammar的词法分析器、语法分析器和语义分析器。
l 设计Context-free Grammar的文法,制作能够生成编译器代码的编译器(即CGCompiler)。
l 用CGCompiler自动生成Expression Grammar的词法分析器代码和语法分析器代码。
1 引言
1.1 编译器的工作流程
编译器的工作流程可以用图 1.1表示。有人说这只是编译器前端,后端还有生成中间结果、代码优化等等内容。我认为这些都是对语义分析的延续,本文重点在于词法分析和语法分析,语义分析部分虽然也有涉及,但并非重点,请勿计较太多。
Context-free Grammar即上下文无关文法,文法是用来描述编程语言的结构的。有了文法,就可以制作对应的编译器。大部分编程语言都是用上下文无关文法描述的。
1.2 加减乘除表达式的文法
举个加减乘除运算表达式(Expression Grammar)的文法的例子,如代码段 1.1所示。
<Expression> ::= <Multiply> <PlusOpt>; <PlusOpt> ::= "+" <Multiply> | "-" <Multiply> | null; <Multiply> ::= <Unit> <MultiplyOpt>; <MultiplyOpt> ::= "*" <Unit> | "/" <Unit> | null; <Unit> ::= number | "(" <Expression> ")"; |
这个Expression Grammar能够表示所有带加减乘除和小括号的数学运算表达式。比如:
表达式示例
关于文法的详细解释参见我之前的随笔
(http://www.cnblogs.com/bitzhuwei/archive/2012/10/22/SmileWei_Compiler.html)
1.3 本文工作内容
本文要做的工作如下:
l 手动写Expression Grammar的词法分析器、语法分析器和语义分析器。
l 设计Context-free Grammar的文法,制作能够生成编译器代码的编译器(即CGCompiler)。
l 用CGCompiler自动生成Expression Grammar的词法分析器代码和语法分析器代码。
有人问,语义分析器呢?答:语义分析器目前还无法自动生成,不过其编写还是有一些规律可循的,相信大家随本文写一个之后就了解了。
2 编译器工作流基类库
由于各种Context-free Grammar的编译器的工作流程都是一样的,所以先把这个工作流程抽象为一个类库。
2.1 源代码(SourceCode)
源代码用System.String就行了。
2.2 单词(Token)和单词列表(TokenList)
单词类和单词列表类设计如图 2.1所示。单词列表就是单词流,流这个概念太概念了,不如列表来的直接,就是一个数组而已。本文就用单词列表来指代单词流。
单词类Token是泛型的,泛型参数应该填入那个描述具体语言的单词类型的enum类型。
2.3 语法树(SyntaxTree)
语法树类的类图如图 2.2所示。
语法树类也是泛型的,泛型参数有三个。第一个应该和Token的泛型参数类型相同。第二个应该填入那个描述具体语言的语法树结点类型的enum类型。第三个是实现了ICloneable接口和无参数构造函数的类。
2.4 词法分析器接口(ILexicalAnalyzer)
词法分析器接口类型如图 2.3所示。
词法分析器接口类型也是泛型的,泛型参数和Token类型的饭菜参数必须一样。
2.5 词法分析器基类(LexicalAnalyzerBase)
图 2.4词法分析器基类
此基类继承自ILexicalAnalyzer接口,并实现了分析单词流的流程。用户在编写具体语言的词法分析器时,只需要实现NextToken函数就行了。
2.6 语法分析器接口(ISyntaxParser)
语法分析器接口的设计如图 2.5所示。
语法分析器接口也是泛型的,泛型参数和SyntaxTree的泛型参数类型、顺序都要相同。
2.7 语法分析器基类(SyntaxParserLL1Base)
语法分析器基类的设计如所示。
图 2.6语法分析器基类
语法分析器基类继承了ISyntaxParser接口,其泛型参数和ISyntaxParser的泛型参数的类型、顺序都必须相同。
2.8 基类库的使用方法
介绍完了这个基类库的核心数据结构,下面示范一下使用方法。
仍以上文的加减乘除表达式的文法(Expression Grammar)为例。
2.8.1 创建单词的枚举类型(EnumTokenTypeExpression)
图 2.7EnumTokenTypeExpression
作为例子,我们要总结出一般性的东西。
对于文法Expression Grammar,其单词的枚举类型包括
Ø Unknown和token_startEnd。token_startEnd是为方便判定单词列表结束用的,类似字符串里的’\0’这个东西。
Ø 关键字token_Plus_,token_Minus_,token_Multiply_,token_Divide_,token_LeftParentheses_,token_RightParentheses_。
Ø 可选的系统性关键字epsilon和number。
那么,对于某文法(Some Grammar,简称SG),其单词的枚举类型包括
Ø 固定都有的Unknown和token_startEnd。
Ø 文法中用双引号引起来的都是关键字,例如Expression Grammar里的"+"、"-"、"*"、"/"、"("和")"。为避免单词类型的枚举名称和C#关键字冲突,本文规定关键字的枚举名称都附以前缀token_。
Ø 可选的系统性关键字就是null、number、identifier和constString,Expression里只出现了null和number,我们就加入这两个到Token类型里。稍候我们会认识到,这四个所谓的“系统性关键字”其实是上下文无关文法的文法(Context-free-Grammar Grammar)的关键字。
2.8.2 创建语法树结点的枚举类型(EnumVTypeExpression)
图 2.8EnumVTypeExpression
作为例子,我们要总结出一般性的东西。
为了方便识别,本文规定,语法树结点的叶结点类型都以Leave结尾。
对于文法Expression Grammar,其语法树结点的枚举类型包括
Ø Unknown和token_startEndLeave。
Ø 叶结点对应的关键字token_Plus_Leave,token_Minus_Leave,token_Multiply_Leave,token_Divide_Leave,token_LeftParentheses_Leave,token_RightParentheses_Leave。
Ø 可选的系统性关键字epsilonLeave和numberLeave。
Ø 非叶结点对应的关键字Expression,PlusOpt,Multiply,MultiplyOpt和Unit。
那么,对于某文法(Some Grammar,简称SG),其单词的枚举类型包括
Ø 固定都有的Unknown和token_startEndLeave。
Ø 文法中用双引号引起来的都是叶结点对应的关键字,例如Expression Grammar里的"+"、"-"、"*"、"/"、"("和")"。为避免语法树叶结点类型的枚举名称和C#关键字冲突,本文规定语法树叶结点的关键字的枚举名称都附以前缀tail_。
Ø 可选的系统性关键字就是null、number、identifier和constString,Expression里只出现了null和number,我们就加入这两个到Token类型里。系统性关键字也是叶结点类型的。
Ø 文法的每个产生式左部就是所有的非叶结点。非叶结点对应的关键字类型名称就用自己原来的名称。为避免语法树非叶结点类型的枚举名称和C#关键字冲突,本文规定语法树非叶结点的关键字的枚举名称都附以前缀case_。
2.8.3 创建词法分析器(LexicalAnalyzerExpression)
创建LexicalAnalyzerExpression类,继承LexicalAnalyzerBase这个抽象类,实现它的NextToken抽象方法。NextToken要做的事就是:判定当前字符的类型,然后得到下一个单词(Token),返回之。看一下代码段 2.1就知道了,很简单。
实现NextToken方法
当然,要实现NextToken函数,还需要为其分别实现获取加号(“+”)、减号(“-”)等单词的子函数。其中获取加号的函数如代码段 2.2所示。
获取加号的GetPlus函数
获取其它类型的Token的方法都是这样的模式,不再详述。词法分析器会不断调用NextToken函数,直到将源代码分析完毕。
2.8.4 创建语法分析器(SyntaxParserExpression)
创建LL1SyntaxParserExpression类,继承LL1SyntaxParserBase这个抽象类,实现它的InitMap和Reset抽象方法。InitMap要做的事就是:设置LL1分析表。Reset要做的事情就是:重置语法分析器到初始状态,这样就可以重新进行语法分析。Reset是细节问题,我本来可以写得更易用,现在没心情,暂时就这样。LL1分析表是LL1语法分析的核心数据结构,后文中详细说明其构造方法。
2.8.5 创建语义分析器
每个编译器的语义分析结果都有所不同,同一个文法也可以创建不同的语义分析器,从而得到不同的语义分析结果。对于Expression Grammar,我们以得到其最终的计算结果为语义分析器的分析结果(就是算出这个式子的值)。
Expression Grammar的编译器的语法树类型是
SyntaxTree<EnumTokenTypeExpression, EnumVTypeExpression, TreeNodeValueExpression>
我们给这个类型添加一个扩展方法(扩展方法的概念请自行百度,很简单)GetValue,使其通过调用GetValue来获取语义分析的结果。GetValue的定义如代码段 2.3所示。
Express Grammar文法的语义分析器
代码段 2.3Expression Grammar文法的语义分析器
2.9 使用创建好的编译器
使用起来就很简单了,如代码段 2.4所示。
使用Expression Grammar的编译器
代码段 2.4使用Expression Grammar的编译器
输出结果很不错吧,如图 2.9所示。
图 2.9输出结果Expression Grammar编译器
3 词法分析原理
词法分析应用了“有限自动机”(同“有限状态机”)的理论。但是其实直接看代码段 2.1的内容,这个过程就是:看第一个字符是什么类型的,然后就知道这个字符及其后面若干个字符会可能组成哪一个或哪几个单词,然后一个字符一个字符地拼接出这个单词来。这时候指针就指到了下一个单词的第一个字符,重复上述过程。自动机理论想学就自己学学,不想学也没事。
词法分析过程可以和计算机网络7层协议里的数据链路层的功能相对比。数据链路层把可能出错的物理层的数据打包成不会有错的数据报,供上层协议继续分析。词法分析器将纯字符串的源代码变成一个个具有内容、类型和顺序的单词,减轻了语法分析的复杂性。
4 语法分析原理
4.1 LL1分析过程
为利于理解,本文设定给出的Expression的表达式都是正确的,能够得到完整的语法树。
下面,对照着之前的博文
(http://www.cnblogs.com/bitzhuwei/archive/2012/10/22/SmileWei_Compiler.html)
文末的【(19 + 18) * (19 - 18)】的语法树,我们来理解LL1分析法的分析过程。
首先,(19 + 18) * (19 - 18)整个式子对应Expression这个非叶结点,这是毋庸置疑的;同时,设置一个指针指向单词列表的第一个元素(即第一个”(”)根据Expression Grammar,Expression结点产生了Multiply和PlusOpt两个子结点,而Multiply又产生了Unit和MultiplyOpt两个子结点。Unit有两种产生子结点的方式,那么应该选哪个呢?选择的规则是这样的:Unit这个非叶结点遇到”(”这个叶结点,说明后面的单词列表会出现一个在”(”和”)”里的子表达式(若Unit遇到number类型的叶结点,那就说明后面的单词就仅仅是一个简单的数值)。所以Unit应该产生”(”、Expression和”)”这三个字结点。这时,第一个”(”处理掉了,指针前进一,指到了number类型的单词(第一个”19”);刚刚Unit产生了Expression结点,此Expression只能继续产生Multiply和PlusOpt子结点,Multiply只能产生Unit和MultiplyOpt子结点。这次的Unit遇到的是number类型的单词,所以我们让它产生number类型的子结点,number类型的子结点的值保存这个单词的内容(即19这个数值)。
这样,指针又前进一,指到了”+”这个单词;这次是MultiplyOpt遇到了”+”,根据Expression Grammar,MultiplyOpt有三种产生子结点的可能情况,第一种和第二种的第一个单词分别是”*”和”/”,不可能与”+”匹配(注意这个判定具有普遍意义),只有第三种才行。所以,就选择让这个MultiplyOpt产生null子结点。
4.2 LL1分析表
分析思路就是这样。注意要点来了,这个分析的关键在于,对应当前的非叶结点和它遇到的单词列表的当前项(指针指向的那个元素),如何判定应该选择哪个候选式(例如Unit要考虑是选择number还是选择"(" <Expression> ")")。所以我们需要知道,每一个非叶结点和每一个单词类型相遇的时候,应该选择哪个候选式。这就是一张二维表了,纵方向上是非叶结点,横方向上是单词枚举类型的所有枚举值,横纵方向相交的内容就是某非叶结点遇到某单词类型时应该选择的候选式。
仍以Expression Grammar为例,为书写方便,给Expression Grammar的各个候选式依次编号,如代码段 4.1所示。
<Expression> ::= <Multiply> <PlusOpt>; // 1 <PlusOpt> ::= "+" <Multiply> | "-" <Multiply> | null; // 2 3 4 <Multiply> ::= <Unit> <MultiplyOpt>; // 5 <MultiplyOpt> ::= "*" <Unit> | "/" <Unit> | null; // 6 7 8 <Unit> ::= number | "(" <Expression> ")"; // 9 10 |
如果还不知道候选式是什么的,请查阅
(http://www.cnblogs.com/bitzhuwei/archive/2012/10/22/SmileWei_Compiler.html)
并重读本文或重读本文。
下面的表 4.1、表 4.2和表 4.3就是传说中的Expression Grammar的LL1分析表。因为单词类型有点多,所以分成三张表了,其实就是一张表。
token_Plus_ (“+”) |
token_Minus_ (“-” ) |
token_Multiply_ (“*” ) |
Divide_ (“/” ) |
epsilon (null) |
|
Expression |
|||||
PlusOpt |
2 |
3 |
|||
Multiply |
|||||
MultiplyOpt |
8 |
8 |
6 |
7 |
|
Unit |
token_LeftParentheses_ (“(” ) |
token_RightParentheses_ (“)” ) |
number (number) |
|
Expression |
1 |
4 |
1 |
PlusOpt |
|||
Multiply |
5 |
5 |
|
MultiplyOpt |
8 |
||
Unit |
10 |
9 |
unknown |
token_startEnd_ (“#” ) |
|
Expression |
|
|
PlusOpt |
4 |
|
Multiply |
||
MultiplyOpt |
8 |
|
Unit |
可以看到,LL1分析表显得有点空荡荡的,只有少数的非叶结点遇到某些单词类型才能推导。其它空白的部分都是出现了语法错误的情况。前文说过,这里我们只讨论正确的情况,语法错误情况太多,而且不是重点;等正确的表达式能够解析的时候,对各种语法错误也就可以分析了。
最后那个“#”是在单词列表末尾添加的判定单词列表结束的标志,作为单独的一个单词枚举值存在。如果大家知道C语言里用char*表示字符串时"\0"的伟大意义,就能知道这里的"#"的伟大意义了。
4.3 LL1分析过程的代码实现
有了LL1分析表,就可以用代码实现分析过程了。分析过程就是:首先将创建Expression结点,Expression结点入栈,指针指向单词列表第一个元素;然后,出栈,在LL1分析表中找到出栈的结点遇到指针指向的单词类型时应该调用的候选式,调用此候选式,从而产生新的结点,链接到语法树上,同步移动指向单词列表的指针;将新产生的结点入栈,再次执行出栈操作……
具体代码如代码段 4.2所示。
LL1分析函数
4.4 LL1分析表的代码实现
对于一个文法描述的编程语言,其文法是固定不变的(语言版本升级除外,那等于创建了一个新的语言了),因此其单词和语法树结点的枚举类型都是固定不变的,因此其LL1分析表也是固定不变的。所以我们可以用一个static的二维数组表示这个LL1分析表。数组元素的类型为一个委托。这样,通过查找分析表,就可以直接找到应该调用的分析函数了。
由于实际中使用的委托是包括三个参数的泛型委托,名字太长,调试的时候根本看不到被调用的委托名字,很麻烦,所以我们创建一个类来代替包裹这个委托,如图 4.1所示。
看图就知道名字的确太长了。这样包裹一下,调试的时候就能看到委托函数的名称了。
5 语义分析原理
语义分析基本思想就是遍历语法树。就这么简单!
回顾代码段 2.3的GetValue的实现,你会发现就是这样的吧。
不过未必是用递归的方式来遍历语法树。比如,编写文法本身的规则就是一种编程语言(文法有编写规则,你可以写出不知道多少种文法代码来),所以,我们想为上下文无关文法(Context-free Grammar)的文法(Context-free-Grammar Grammar)创建编译器,此编译器的输出结果是文法的词法分析器代码和语法分析器代码!这是本文最终的目的,所幸得以实现。这个实现方式就不是用递归的方式做语义分析的。
没错,你说的对,Lex和YACC已经做好了这项工作,但是我不想学新的编程语言了(C#和VS2010太好用,被惯坏了),而且我想自己写出来,这样才能真正掌握编译原理。重复造轮子在学习研究的过程中还是有益的。
6 Context-free Grammar的编译器设计
CGGrammar是(Context-free-Grammar Grammar)的简写,我们就用CG作为CGGrammar的编译器名字。
6.1 CGGrammar的文法
有了编写Expression Grammar及其编译器的经验,我们现在已经可以顺利写出CG的文法了。开始的时候,我试图一步到位,直接写上下文无关文法的文法(Context-free-Grammar Grammar),发现太费脑子了,大脑内存严重不足!于是踏踏实实地从Expression的文法和编译器开始写,终于成功了!)
CG的文法如代码段 6.1所示。
<Start> ::= <Vn> "::=" <VList> ";" <PList>; <PList> ::= <Vn> "::=" <VList> ";" <PList> | null; <VList> ::= <V> <VOpt>; <V> ::= <Vn> | <Vt>; <VOpt> ::= <V> <VOpt> | "|" <V> <VOpt> | null; <Vn> ::= "<" identifier ">"; <Vt> ::= "null" | "identifier" | "number" | "constString" | identifier | number | constString; |
用文法描述文法本身,的确有点绕。
这个文法可以算是本文的精髓了!你是不知道我费了多少张草稿纸才把这个正确的形式写出来!即使是Expression那么简单的文法,我也修改了好多次。学习这种事,还是只有反复实践才能领悟到东西。
有了文法,我们就可以按部就班,完全仿照着写Expression的编译器那样步骤,做出这编译器的编译器了!想想只要做出这个东西,就能随意生成自己的编译器,尼玛爽爆了有木有!
6.2 CG的单词枚举类型
根据2.8.1的分析,我们可以轻松写出CG的单词枚举类型,如图 6.1所示。
图 6.1CG的单词枚举类型
看到了吧,看到了吧,看到identifier和token_identifier潜藏的冲突了吧!知道”token_”前缀的重要性了吧!想象到”tail_”和”case_”也是这样的意义了吧!
6.3 CG的语法树结点枚举类型
根据2.8.2的分析,语法树结点的枚举类型也很好写。
图 6.2CG的语法树结点枚举类型
类似单词的枚举类型,我们用”tail_”和”case_”这样的前缀避免了名称的冲突问题。当然,你想用”crapWriter”什么的当做前缀都没问题。
6.4 CG的词法分析器
类似2.8.3中的步骤,创建LexicalAnalyzerCG类,继承LexicalAnalyzerBase这个抽象类,实现它的NextToken抽象方法。NextToken的结构与代码段 2.1相同,不再浪费篇幅。只说一下获取标识符(GetIdentifier)这个稍微有点特殊的子函数好了。由于编程语言的字母型关键字(char、typeof等等)是特殊的标识符,所以GetIdentifier在最后要识别一下得到的标识符是不是关键字,如代码段 6.2所示。
获取标识符和关键字的函数
6.5 CG的语法分析器
类似2.8.4中的步骤,创建LL1SyntaxParserCG类,继承LL1SyntaxParserBase这个抽象类,实现它的InitMap和Reset抽象方法。
这里关键也是做好CG的LL1分析表来。有了4.2的示范,大家可以照猫画虎自己做了,我偷个懒,不再浪费篇幅。
7 CG的语义分析
最终高潮来了亲们!
给定文法,那么它的词法分析器和语法分析器就确定了。这是毋庸置疑的。但是其实现过程却也路漫漫其修远兮,写点东西不容易。
仍以Expression为例,说明Expression编译器的词法分析器和语法分析器代码是如何自动生成的。
7.1 获取Expression的文法数据结构
文法由一个或多个产生式以及文法名称(第一个产生式左部的名称)构成。
产生式由左部和右部构成。
右部由一个或多个候选式构成。
候选式由一个或多个结点构成。
结点由一个非终结点或一个终结点构成。非终结点由坐尖括号“<”、标识符(identifier)和右尖括号“>”构成,由于尖括号固定不变,不需要为其设置字段。终结点由标识(identifier)符构成。
由此可以得到描述文法的类图,如图 7.1所示。
从字符串格式的文法到这样的数据结构描述的文法是语义分析的第一步。后续步骤都是通过操作ContextfreeGrammar类及其相关类来实现的。
7.2 消除左递归
消除左递归包括消除直接左递归和间接左递归两点。
我觉得吧,写文法的人应该自己避免文法具有左递归的写法。如果连这一点都做不到,那就等于说他不理解编译原理,那也肯定写不出语义分析器来了。另一方面,用电脑来消除左递归时,会自行添加和去掉某些非叶结点,那么非叶结点的名字就不好整了,文法作者都不认识,以后写语义分析器就难以下手。所以消除左递归这部分我没有写代码实现,仅介绍其概念和算法。
7.2.1 消除直接左递归
设P -> Pα1 | Pα2 | ... | Pαn | β1 | β2 | ... |βm
其中每个α不为ε(ε就是空,什么都没有的意思,类似null),每个β不以P开头。
则非终结符P可改写为
P -> β1P’ | β2P’ | ... | βmP’
P’ -> α1P’ | α2P’ | ... | αnP’
解释:原来的P展开就是βxαi..αiαj..αj...αt..αt的形式,即某个β开头,各种阿尔法跟随的一个串。所以与改写形式所表达的东西是一样的。
7.2.2 消除间接左递归
给定文法G,若G不含回路(P经过若干步推导又得到P)且不含以ε为右部的产生式。
给定文法G,若G不含回路(P经过若干步推导又得到P)且不含以ε为右部的产生式。
则其消除左递归的算法如代码段 7.1所示。
1) 对G的非终结符按任意顺序排列,如A1, A2, A3, ... , An 2) for (i = 1; i <= n; i++) 3) 简化由上一步得到的文法,即去掉多余的规则 |
7.3 计算FIRST集
FIRST集是生成LL1分析表的必要数据,所以没法绕开。(除非你用别的语法分析器生成方法)
FIRST集的含义是:候选式经过推导,最后就是一个终结符的串;推导过程不同,会有多个不同的串(可能是无限个),这些串里的第一个字符组成的集合就是这个候选式的FIRST集。有了这个FIRST集,就可以知道这个候选式是否能匹配接下来要解析的单词列表了。
获取ContextfreeGrammar类的FIRST集的算法实现如代码段 7.2所示。
计算文法的FIRST集
FIRST集的求法相对FOLLOW集简单,大家多看看代码就好。其实,就是看一个非叶结点A可能推导出什么结点BCD…,推导出的结点B的FIRST集当然也属于此非叶结点A的FIRST集了;如果推导出的结点B能够推导出“null”结点,说明C的FIRST集也属于此非叶结点A;依此类推即可。
7.4 计算FOLLOW集
FOLLOW集也是生成LL1分析表的必要数据,所以还是没法绕开,必须啃掉。
设上下文无关文法(二型文法)G,开始符号为S,对于G中的任意非终结符A,其FOLLOW(A) = { a | S 经过0或多步推导会出现 ...Aa...的形式,其中a∈VT或#号 }
解读:FOLLOW集的含义是:G的一切句型中,能够紧跟着非终结符A之后的一切终结符或井号#。#是当出现 ...A 这样的情况,即A为最后一个字符。
构造FOLLOW集的算法如代码段 7.3所示。
1) 令#∈FOLLOW(S) 2) 若文法G中有形如A –> αBβ的规则,且β≠ε,则将FIRST(β)中的一切非终结符加入FOLLOW(B) 3) 若文法G中有形如A -> αB或A -> αBβ的规则,且ε∈FIRST(β),则将FOLLOW(A)中的全部元素加入FOLLOW(B) 4) 反复使用前两条规则,直到所有的FOLLOW集都没有改变。 |
构造FOLLOW集的代码实现如代码段 7.4所示。
构造FOLLOW集的函数
7.1 生成词法分析器
这里写起来没意思了,懂的自然懂,不懂的写了也看不懂。而且生成词法分析器本来也相对简单。
7.2 生成语法分析器
生成语法分析器的关键部分是生成LL1分析表这个数据结构,并为之初始化各个位置应有的元素。当我们得到了文法(Grammar,简写为G)的FIRST集和FOLLOW集,就能够构造G的LL1分析表,具体算法如代码段 7.5所示。
输入:文法G 输出:G的LL(1)分析表M(Ax, ay),其中A为非终结符,a为终结符 算法: 1) 求出G的FIRST集和FOLLOW集 2) for (G的每个产生式 A -> γ1 | γ2 | ... | γm) |
有了这张表,生成语法分析器的代码就是单纯的体力活了,不再啰嗦,只给出其实现代码如代码段 7.6所示。
计算LL1分析表的函数
7.3 生成其它杂碎
杂碎不等于可以忽视,代码这种东西,没有哪部分是可有可无的。
单词的枚举类型的代码,只需遍历文法中的关键字,外加文法中出现的系统性关键字和unknown、startEnd这两个必有项即可。语法树结点的枚举类型则再加上非叶结点即可。注意别忘记写前缀。
8 自动生成Expression编译器
8.1 生成代码
打开Winform的集成工具如图 8.1所示。填入编译器名字、代码命名空间、代码文件存放文件夹、文法,点击“确定”,开始生成。
图 8.1Context-free Grammar的编译器代码生成器
代码生成完成,如图 8.2所示。
图 8.2Expression Grammar的编译器代码生成完成
生成的代码文件如图 8.3所示。
8.2 使用生成的代码
用VS2010创建一个类库项目,把生成的代码添加进来,词法分析器和语法分析器就做好了,如图 8.4所示。
剩下的只是写语义分析器的事情了,本例中就是编写SyntaxTreeExpressionGetValue.cs这个文件,此文件为
SyntaxTree<EnumTokenTypeExpression, EnumVTypeExpression, TreeNodeValueExpression>
实现了一个扩展方法,用以得到表达式的值。
你要知道,一边写代码一边查API这种事是VS尽力避免了的。我这里也效仿了一下,自动生成的代码里已经包含了必要的重要的没它真不行的注释,如图 8.5所示。
你可以从自动弹出来的注释里看到这个函数是做什么用的,它是对哪个非叶结点遇到哪个单词类型时调用的函数,它应该根据哪个候选式为这个非叶结点添加子结点。就是说,这个弹出注释给出了FIRST集的内容和LL1分析表的内容,有了这个,写语义分析器就很容易了。
8.3 验证自动生成的编译器类库
写完语义分析器,Expression的编译器类库就大功告成了。下面来验证一下。
创建一个Console的项目,在Main函数里写测试用例,如代码段 8.1所示。
验证自动生成的编译器类库
验证结果如图 8.6所示。
9 后记
今年还是去年过年的时候我开始写这个项目,纯粹为了弄懂编译原理。没想到真的把这一路走通了。最近一周认识到处理关键字遇到的混乱情况,修补了不能识别关键字的缺憾,同时完善了一些小地方。
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |