zoukankan      html  css  js  c++  java
  • 一个编译器的实现0

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

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

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

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

    上图展示一下先。

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

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

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

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

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

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

    图4 自动打印语法树

    图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

     

  • 相关阅读:
    CF547D Mike and Fish
    CF147B Smile House
    [BJOI2017]树的难题
    AT2306 Rearranging
    复利计算器--单元测试
    操作系统 实验1 命令解释程序的编写
    个人项目耗时对比记录表
    复利计算器3.0
    0320记《构建之法》读后感
    复利计算实验总结
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/SmileWei_Compiler.html
Copyright © 2011-2022 走看看