# CGCompiler **Repository Path**: bookcases/CGCompiler ## Basic Information - **Project Name**: CGCompiler - **Description**: No description available - **Primary Language**: C# - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-07-07 - **Last Updated**: 2021-07-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # CGCompiler Context-free Grammar Compiler is a compiler-compiler in C# that generates lexical analyzer and syntax parser automatically according to the grammar you defined.

前一阵做了个编译器(仅词法分析、语法分析、部分语义分析,所以说是前端),拿来分享一下,如有错误,欢迎批评指教!

整个代码库具有如下功能:

提供编译器所需基础数据结构、计算流程框架类,可供继承使用;
提供基础数据结构的可视化控件;
提供类似YACC的词法分析器、语法分析器自动生成功能;
提供Winform程序,集成和扩展上述功能,方便研究和应用。

本文及其后续系列将逐步给出所有工程源代码(visual studio 2010版本)。

上图展示一下先。

图1 词法、语法分析和结点匹配

图2 自动生成词法分析器、语法分析器

图3 自动生成词法分析器、语法分析器

图4 自动打印语法树

为了说清楚编译器这种东西,我想最好还是举例。

比如我们要为数学计算的表达式(Expression)设计一个编译器。(当然有很多方法可以实现读取数学表达式并计算结果的算法,未必使用编译原理)

来看一些数学表达式的例子:

37

19 * 19 - 18 * 18

(19 + 18) * (19 - 18)

18 +19 / (18 / 18)

a  + (a + 1) + (a + 2) + (a + 3)

好了够了,大家能够了解本文所讨论的Expression的范围了。那么我们引入“文法”(Grammar)的概念。Expression的文法就是这样的:

<Expression> ::= <Multiply> <PlusOpt>;
<PlusOpt> ::= "+" <Multiply> | "-" <Multiply> | null;
<Multiply> ::= <Unit> <MultiplyOpt>;
<MultiplyOpt> ::= "*" <Unit> | "/" <Unit> | null;
<Unit> ::= identifier | "(" <Expression> ")" | number;

我们分别展示出上述几个例子用文法展开的过程。

37: <Expression>

=> <Multiply> <PlusOpt>

=> <Unit> <MultiplyOpt>

=> number

19 * 19 - 18 * 18: <Expression>

=> <Multiply> <PlusOpt>

=> <Unit> <MultiplyOPt> "-" <Multiply>

=> number "*" <Unit> "-" <Unit> <MultiplyOpt>

=> number "*" number "-" number "*" <Unit>

=> number "*" number "-" number "*" number

(19 + 18) * (19 - 18): <Expression>

=> <Multiply> <PlusOpt>

=> <Unit> <MultiplyOpt>

=> "(" <Expression> ")" "*" <Unit>

=> "(" <Multiply> <PlusOpt> ")" "*" "(" <Expression> ")"

=> "(" <Unit> <MultiplyOpt> "+" <Multiply> ")" "*" "(" <Multiply> <PlusOpt> ")"

=> "(" number "+" <Unit> <MultiplyOpt> ")" "*" "(" <Unit> <MultiplyOpt> "-" <Multiply> ")"

=> "(" number "+" number ")" "*" "(" number "-" number <MultiplyOpt> ")"

=> "(" number "+" number ")" "*" "(" number "-" number ")"

写到这里就,其余例子大家自己试试~如果写不出来,后面的部分可能就不太容易看了。(试试写写,很快就写的比较熟练了)

 

总结一下“文法”(Grammar)。文法就是描述Expression的构成的,和英语的语法类似吧。 有了文法,我们就可以写编译器了。

Expression的文法有5个式子,这5个式子就叫做“产生式”(Production),因为他们能从左边的结构产生(推导)出右边的结构来。一个文法至少有一个产生式,第一个产生式的左边的结点是初始结点,所有的推导都必须从初始结点(即第一个产生式)开始。

产生式(Production)左边叫做左部(左部只有始终一个结点),右边叫做右部(废话),中间用【::=】这个符号隔开。

右部由符号【|】分为若干部分,每一部分都是产生式可能推导出的一个结果,且每次只能选择其中一个进行推导。【null】表示什么也不推导出来。(这是个霸气的符号,不要觉得什么都不推导出来就不重要,恰恰相反,这个符号很重要)

为简化后文的说明,继续举例:<PlusOpt> ::= "+" <Multiply> | "-" <Multiply> | null;

对于这个产生式,其实是由三部分<PlusOpt> ::= "+" <Multiply>;和<PlusOpt> ::= "-" <Multiply>和<PlusOpt> ::= null;组成的,每一部分都称为一个“推导式”(Derivation)。

像【(19 + 18) * (19 - 18)】这样一个具体的“东西”,我们称之为一个“句子”(Sentence)。

明了了上述关于文法的东西,就可以进行编译器的设计了。

 

我们先搞搞清楚,编译器能做什么?以Expression的【19 * 19 - 18 * 18】为例,Expression的编译器首先要读取字符串格式的源代码,即:

1 var sentence = “19 * 19 - 18 * 18”;
2 var expLexicalAnalyzer = new LexicalAnalyzerExpression();
3 expLexicalAnalyzer.SetSourceCode(sentence);

 

然后,编译器进行词法分析,得到单词流(TokenList)。“流”这个东西,其实就是数组。

1 var tokens = expLexicalAnalyzer.Analyze();

在此例中,得到的单词流是这样的:

[19]$[Number]$[0,0]$[False]$[]
[*]$[Multiply]$[0,3]$[False]$[]
[19]$[Number]$[0,5]$[False]$[]
[-]$[Minus_]$[0,8]$[False]$[]
[18]$[Number]$[0,10]$[False]$[]
[*]$[Multiply_]$[0,13]$[False]$[]
[18]$[Number]$[0,15]$[False]$[]

第一个单词的意思是:这个单词是【19】,类别是【Number】,在源代码中第一个字符的位置是【行0, 列0】,是否错误的单词【False】,其它描述信息为【】(空,即木有描述信息))

然后是根据这个单词流分析出语法树:

1 var expSyntaxParser = new SyntaxParserExpression();
2 expSyntaxParser.SetTokenList(tokens);
3 var syntaxTree = expSyntaxParser.Parse();

得到的语法树是一个树的结构,可以表示如下:

<Expression>
  ├─<Multiply>
  │  ├─<Unit>
  │  │  └─number(19)
  │  └─<MultiplyOpt>
  │     ├─*
  │     └─<Unit>
  │        └─number(19)
  └─<PlusOpt> 
     ├─- 
     └─<Multiply> 
        ├─<Unit> 
        │  └─number(18) 
        └─<MultiplyOpt> 
           ├─* 
           └─<Unit> 
              └─number(18)

从此树中可以看到,树的结构和上文的文法展开过程是对应的,并且树的叶结点从上到下组成了我们的例子【19 * 19 - 18 * 18】

然后就是语义分析了。到目前为止(据我所学到的),人类还没有完善的自动生成语义分析代码的能力。我们在此处就把”计算结果“作为语义分析的任务。仍以上例进行说明。各个叶结点的含义我们是知道的,【+】【-】【*】【/】代表运算,【number】代表数值,【identifier】代表变量名。那么在没有【identifier】的时候,数和数就直接算出结果来,有【identifier】就保留着不动。我们分别为Expression文法的各类结点都赋予语义:

<Expression>:将它的两个子结点进行运算或保留。

<Multiply>:将它的两个子结点进行运算或保留。

<PlusOpt>:去掉自己,用自己的子结点代替自己的位置。

<Unit>:去掉自己,用自己的子结点代替自己的位置。

<MultiplyOpt>:去掉自己,用自己的子结点代替自己的位置。

“+”:对自己的左右结点进行加法运算。

“-”:对自己的左右结点进行减法运算。

“*”:对自己的左右结点进行乘法运算。

“/”:对自己的左右结点进行除法运算。

identifier:保持不变。

number:保持不变。

“(“:若自己右部的<Expression>成为数字或单一的【identifier】,则去掉自己,去掉<Expression>右部的”)”;否则不变。

“)”:保持不变。

上例经过语义分析(对语法树自顶向下进行递归分析其语义),就得到一个数值”37“。

语义分析的伪代码如下:

语义分析伪代码
 1 SyntaxTreeExpression SemanticAnalyze(SyntaxTree root)
 2 
 3 {
 4 
 5     switch(root.NodeType)
 6 
 7     {
 8 
 9     case EnumTreeNodeType.Expression:
10 
11           return Cacul(SemanticAnalyze(root.Children[0]),SemanticAnalyze(root.Children[1]));
12 
13           break;
14 
15     case EnumTreeNodeType.Multiply:
16 
17           return Cacul(SemanticAnalyze(root.Children[0]),SemanticAnalyze(root.Children[1]));
18 
19           break;
20 
21     case EnumTreeNodeType.PlusOpt:
22 
23           var child = SemanticAnalyze(root.Children[0]);
24 
25           var child2 = SemanticAnalyze(root.Children[1]);
26 
27           root.parent.Children[1] = child; root.parent.Children[2] = child2;
28 
29           break;
30 
31     case EnumTreeNodeType.Unit:
32 
33           root.parent.Children[0] = root.Children[0];
34 
35           break;
36 
37     //
38 
39     case EnumTreeNodeType.Plus:// “+”
40 
41           return Calcu(SemanticAnalyze(root.parent.Children[0]), SemanticAnalyze(root.parent.Children[2]));
42 
43           break;
44 
45     //
46 
47 }

语义分析完成,我们这个编译器前端也就大功告成了。

所以这个编译器要实现的东西大体感觉就是这样的。虽然单单对Expression进行编译分析是没多大意思的,但是这个例子在足够简单的同时,又足够典型,等我们把这个例子实现了,再复杂的编译器也都能做出来了。编译器制作步骤比较多,工作量也大,如果一上来就抱着完整的C语言文法来做,等于把自己埋在深不见底的BUG海洋中活活淹死。

以后实现了编译器的语法分析后,就可以自动生成示例中的语法树了,其实这也算是一种语义分析。

后面系列文章将给出具体的设计和实现过程,以及完整的工程代码。敬请关注!

关于本系列有什么好的建议,也请提出来,O(∩_∩)O谢谢!

PS:下面给出【(19 + 18) * (19 - 18)】的语法树,供大家学习参考,也方便后续文章讲解。

<Expression>
  ├─<Multiply>
  │  ├─<Unit>
  │  │  ├─(
  │  │  ├─<Expression>
  │  │  │  ├─<Multiply>
  │  │  │  │  ├─<Unit>
  │  │  │  │  │  └─number(19)
  │  │  │  │  └─<MultiplyOpt>
  │  │  │  │     └─null
  │  │  │  └─<PlusOpt>
  │  │  │     ├─+ 
  │  │  │     └─<Multiply>
  │  │  │        ├─<Unit>
  │  │  │        │  └─number(18)
  │  │  │        └─<MultiplyOpt>
  │  │  │           └─null
  │  │  └─)
  │  └─<MultiplyOpt>
  │     ├─*
  │     └─<Unit>
  │        ├─(
  │        ├─<Expression>
  │        │  ├─<Multiply>
  │        │  │  ├─<Unit>
  │        │  │  │  └─number(19)
  │        │  │  └─<MultiplyOpt>
  │        │  │     └─null
  │        │  └─<PlusOpt>
  │        │     ├─-
  │        │     └─<Multiply>
  │        │        ├─<Unit>
  │        │        │  └─number(18)
  │        │        └─<MultiplyOpt>
  │        │           └─null
  │        └─)
  └─<PlusOpt> 
     └─null

 

关于编译原理基础概念可参考http://www.cnblogs.com/bitzhuwei/archive/2012/10/22/SmileWei_Compiler.html 

关于下列代码的基础数据结构参见http://www.cnblogs.com/bitzhuwei/archive/2012/03/09/compiler_basic_data_structure.html

一、      消除直接左递归

设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的形式,即某个β开头,各种阿尔法跟随的一个串。所以与改写形式所表达的东西是一样的。

 

二、      消除间接左递归

给定文法G,若G不含回路(P经过若干步推导又得到P)且不含以ε为右部的产生式。

则其消除左递归的算法如下:

  1. 对G的非终结符按任意顺序排列,如A1, A2, A3, ... , An
  2. for (i = 1; i <= n; i++)
        for (j = 1; j <= i - 1; j++)
        {
            把形如Ai -> Ajγ的产生式改写成Ai -> δ1γ | δ2γ | ... | δkγ的形式,其中Aj -> δ1 | δ2 | ... | δk是关于Aj的全部规则
            消除Ai规则中的直接左递归
        }
  3. 简化由上一步得到的文法,即去掉多余的规则

 

三、      FIRST集

若文法G为二型文法且不含左递归,则G的非终结符的每个候选式α的终结首符集FIRST(α)为FIRST(α) = { a | α经过0或多步推导为a...的形式,其中a∈VT }

解读:FIRST集的含义是:候选式经过推导,最后就是一个终结符的串,推导过程不同,会有多个不同的串(可能是无限个),这些串里的第一个字符组成的集合就是这个候选式的FIRST集。有了这个FIRST集,就可以知道这个候选式是否能匹配接下来要解析的单词流了。

 

四、      FOLLOW集

设上下文无关文法(二型文法)G,开始符号为S,对于G中的任意非终结符A,其FOLLOW(A) = { a | S 经过0或多步推导会出现 ...Aa...的形式,其中a∈VT或#号 }

解读:FOLLOW集的含义是:G的一切句型中,能够紧跟着非终结符A之后的一切终结符或井号#。#是当出现 ...A 这样的情况,即A为最后一个字符。

 

五、      构造FOLLOW集的算法

  1. 令#∈FOLLOW(S)
  2. 若文法G中有形如A –> αBβ的规则,且β≠ε,则将FIRST(β)中的一切非终结符加入FOLLOW(B)
  3. 若文法G中有形如A -> αB或A -> αBβ的规则,且ε∈FIRST(β),则将FOLLOW(A)中的全部元素加入FOLLOW(B)
  4. 反复使用前两条规则,直到所有的FOLLOW集都没有改变。

 

六、      构造LL(1)分析表的算法

输入:文法G

输出:G的LL(1)分析表M(Ax, ay),其中A为非终结符,a为终结符

算法:

  1. 求出G的FIRST集和FOLLOW集
  2. for (G的每个产生式 A -> γ1 | γ2 | ... | γm)
    {
        if (a ∈ FIRST(γi)) 置 M(A, a) 为 “A -> γi”
        if (ε ∈ FIRST(γi))
            for (每个 a ∈ FOLLOW(A))
                置 M(A, a)为 “A -> γi”(实际上此处的γi都是ε)
    }
    置所有无定义的 M(A, a)为出错。

 

 

一个编译器的实现3——用编译原理自动化制作文本解析器

PS:本文PDF版在这里

 

关于编译器的概念、工作流程、算法和设计方案,可参考这里(http://www.cnblogs.com/bitzhuwei/archive/2013/06/05/CompilerDesignAndImp4Context-freeGrammar.html)。阅读本文须理解“上下文无关文法(Context-free Grammar)”是什么。

本文以加减乘除表达式和一个3D坦克游戏模型为例,说明如何自动生成解析器以及如何使用自动生成的代码。

文末附源代码。

加减乘除表达式

运行编译器代码生成器(bitzhuwei.CGCompiler.Winform.exe),默认配置文件中已经有加减乘除表达式(Expression)的文法了。

clip_image002[8]

设置好编译器名字、命名空间和代码存放的位置,点击“开始!”。

若文法没有错误,会在指定位置生成Expression解析器的代码。

clip_image004[8]

clip_image006[8]

一共生成了10个文件(其中bitzhuwei.CompilerBase.dll和使用说明.txt是直接复制的)。

三个Enum*.cs文件分别是文法的字符类型、单词类型和语法树结点类型。

LexicalAnalyzer*.cs文件是词法分析器。

LL1SyntaxParser*.cs文件是语法分析器。

SyntaxTreeNodeValue*.cs文件是语法树结点类型,稍候会用到。

使用生存的代码的方法很简单:创建一个类库项目,把生成的10个文件全部加进去,引用bitzhuwei.CompilerBase.dll文件。

clip_image007[8]

为了测试,再创建一个Console项目,用下面的代码测试。

测试Expression的代码

 

输入的语法树如下图所示。

clip_image009[8]

我们使用解析器,目的是为了得到数据结构后再获取有价值的结果。Expression的价值在于获取表达式的值,通过遍历语法树获取这个值是很容易的。(这个代码只能自己写,这属于语义分析阶段了,目前还无法自动生成。)

SyntaxTreeExpressionGetValue.cs

 

ArmadaTank模型

坦克舰队(ArmadaTank)是我很喜欢的一款游戏,现在我正在试图用C#重写这个游戏。喜欢的同学可以自行搜索“坦克舰队”。

ArmadaTank的3D模型是用纯文本的*.dtm文件标识的。完全可以用自动生成的解析器来加载之。

步骤就不再说了,和Expression的步骤一样,这里只贴一下DTM文件的文法。

DTM的文法

 

用OpenGL来显示3D模型(语义分析及其之后的阶段),如下图所示。

clip_image011[8]

源代码在此。

http://files.cnblogs.com/bitzhuwei/bitzhuwei.CGCompiler2013-11-20_19-27-00.rar