zoukankan      html  css  js  c++  java
  • 一只小小麻雀——基于语法分析工具Gold开发的加减法解释器

     麻雀虽小,五脏俱全!本文试图用最简单的示例覆盖最多的知识点。文中主要通过加减法器的设计来介绍基于Gold的解释器(关于解释器和编译器的区别联系见《儿子和女儿-解释器和编译器的区别与联系》)开发方法,不仅介绍了基于Gold的词法分析和语法分析,同时还介绍了如何在自动生成的基本骨架代码上来完成语义分析、解释执行和错误提示功能。

     

    1.目标介绍:

      首先说一下,我们开发的解释器究竟是面对什么语言呢?例子非常简单,只是整数的加减法而已,输入源语言如下所示:

      1+2+3

      1  +22+   333+4+ 555

      要求如下:

      (1)输入为32位正整数的和或差的表达式,表达式中终结符的前后和中间可以有不可见字符。

      (2)如果表达式正确,输出其结果,如下所示:

    输入:1   +  2+         3   -   4

    输出:2

      (3)识别词法错误和语法错误:

      输入:1+ a+1+      3+         534

      输出:0行3列位置的字符不应该为a

      (4)识别语义错误。

    输入:1+9999999123456789+123456789

    输出:0行2列:数字太大,超过32位界限

    输入:2123456789+123456789

    输出:0行11列:和太大,超过32位界限

    输入:1-2123456789-123456789

    输出:0行2列:差太小,超过32位界限

     

    2. 内容纲要:

      结合GOLD的开发流程,我决定按如下提纲介绍:

      Part 1:加减法器的词法分析和语法分析;

      Part 2:文法表文件的生成与引擎的介绍;  

      Part 3:GOLD基本骨架源码的结构介绍;

      Part 4:如何添加解释执行代码;

      Part 5:实现错误提示功能(包括词法错误、语法错误、语义错误);

      Part 6:平凡小结——由简而繁

     

    3.加减法器的词法分析和语法分析:

      在前面介绍的Gold的开发方法(《【翻译】语法分析工具Gold介绍(2)——基本开发方法 》)中,我们知道第一步需要做的事情是设计加减法器的设计文法,如下所示:

    !大小写不敏感,开始符号位expression 

    "Case Sensitive" = True
    "Start Symbol"   = <expression>

    !终结符   

    Number  = [123456789]{digit}*

    !BNF产生式
    <expression>         ::= <expression>'+'Number
                              | <expression>'-'Number
                              | Number

      这个产生式文法入门相当容易,够直观。我不多解释,只说一点:在Gold文法中,不可见字符是默认被识别为分隔符的(比如1  2被识别为两个终结符‘1’和‘2’),一般不做理睬。如果你真要睬它,后面的骨架源码中有所介绍。

      

      大家是不是觉得这个文法太简单了。在这里我不得不声明几句:

    • 本文不深入讨论复杂的文法。一只蜂鸟和一只老鹰在生理构造上不会有根本上的不同。
    • Gold帮助用户屏蔽掉了文法规约的内部过程,大大方便了用户。如果不是这样,这多如繁星的语法分析工具要来何用?
    • 本文不是去做一个C、C#、java的编译器或解释器,而是告诉你这样做的基本原理。若你真正明白了纸船浮在水上的原理,即便是钢铁巨轮也不仅仅是个梦想。
    • 本文在小结中会提到一些深入的东东。

     

    3.文法表文件的生成与引擎的介绍

       什么是文法表文件?简单说来,文法表存储了语言产生式导出的所有移进规约表、字符表等等等信息。这里涉及到编译原理中的一些复杂理论。佛祖保佑,幸好我们有语法分析工具。

       不同的文法表文件可以被一个通用的引擎来读取,以便解析不同的语言。 比如,引擎如果读取Java语言所产生的文法表文件,就能解析Java语言编写的所有源码;引擎读取C#语言产生的文法表文件,就能解析C#语言编写的所有程序。

           引擎以类库的形式存在,可以被用户所引用。

       将文法保存到grm文件中,用软件Gold Builder打开,在测试成功后,选择Project=>Save the table,保存文法表文件(cgt格式),以后会被调用。

     

    4.GOLD基本骨架源码的结构介绍

      如何生成骨架源码?文法编译成功后,选择菜单Project=>Create the skeleton program,选择输出语言C#-Calitha engine Event based,输出文件保存为MyParser.cs。

      Gold生成的源码的可读性非常高,这是我喜欢它的一个重要原因。下面主要介绍源码的组成。

      4.1 终结符和非终结符的枚举定义:

        enum SymbolConstants : int
        {
            SYMBOL_EOF        = 0, // (EOF)
            SYMBOL_ERROR      = 1, // (Error)
            SYMBOL_WHITESPACE = 2, // (Whitespace)
            SYMBOL_MINUS      = 3, // '-'
            SYMBOL_PLUS       = 4, // '+'
            SYMBOL_NUMBER     = 5, // Number
            SYMBOL_EXPRESSION = 6  // <expression>
        };

        enum RuleConstants : int
        {
            RULE_EXPRESSION_PLUS_NUMBER  = 0, // <expression> ::= <expression> '+' Number
            RULE_EXPRESSION_MINUS_NUMBER = 1, // <expression> ::= <expression> '-' Number
            RULE_EXPRESSION_NUMBER       = 2  // <expression> ::= Number
        };

      简单明了,主要是为了后面的使用。

     

      4.2 MyPasser类:

        public class MyParser
        {
            private LALRParser parser;//解析引擎
            public MyParser(string filename)//若干构造函数
            public MyParser(string baseName, string resourceName)
            public MyParser(Stream stream)
            private void Init(Stream stream)//初始化,这是在代码骨架中最关键的函数。

            public void Parse(string source)//解析函数

        //读取终结符,同时负责处理终结符在语义上的错误
            private void TokenReadEvent(LALRParser parser, TokenReadEventArgs args)
            //根据终结符创建对象,开发者主要修改的函数

         private Object CreateObject(TerminalToken token)
            //产生式规约,同时负责处理规约是可能出现的异常

        private void ReduceEvent(LALRParser parser, ReduceEventArgs args)
            //根据规约动作创建返回对象,此处添加解释执行代码或者用于生成目标代码的代码,开发者主要修改的函数

       public static Object CreateObject(NonterminalToken token)

        //当语言被识别为正确时,该函数被调用,此处可添加返回结果代码
            private void AcceptEvent(LALRParser parser, AcceptEventArgs args)

        //发生词法错误是被调用,此处添加词法错误提示代码
            private void TokenErrorEvent(LALRParser parser, TokenErrorEventArgs args)

        //发生语法错误时被调用,此处添加语法错误提示代码
            private void ParseErrorEvent(LALRParser parser, ParseErrorEventArgs args)
        }

     

         4.3 一个联系所有组件的函数——MyParser.init()

            private void Init(Stream stream)
            {

        //读取文法表文件
                CGTReader reader = new CGTReader(stream);
                parser = reader.CreateNewParser();
                parser.TrimReductions = false;
                parser.StoreTokens = LALRParser.StoreTokensMode.NoUserObject;

        //将解析引擎的文法规约事件绑定到MyParser类中的事件处理函数ReduceEvent

                parser.OnReduce += new LALRParser.ReduceHandler(ReduceEvent);

        //将解析引擎的终结符读取事件绑定到MyParser类中的事件处理函数TokenReadEvent

                parser.OnTokenRead += new LALRParser.TokenReadHandler(TokenReadEvent);

        //将解析引擎的正确解析源语言事件绑定到MyParser类中的事件处理函数AcceptEvent
                parser.OnAccept += new LALRParser.AcceptHandler(AcceptEvent);

        //将解析引擎的发生词法错误事件绑定到MyParser类中的事件处理函数TokenErrorEvent
                parser.OnTokenError += new LALRParser.TokenErrorHandler(TokenErrorEvent);

        //将发生语法错误事件绑定到MyParser类中的事件处理函数ParseErrorEvent
                parser.OnParseError += new LALRParser.ParseErrorHandler(ParseErrorEvent);
            }

      这个函数起到了中央枢纽的作用,将文法表文件、源语言、解析引擎、各个事件处理函数紧紧的联系在一起。

     

      4.4 骨架源码的使用

      在不破坏骨架源码的骨架结构的前提下,源码可以被任意的修改。当然也包括了MyParser的构造函数。

      可以在任意C#项目中调用生成的解析器MyParser,当然别忘了将文法表文件(本例中是cal1.cgt)、引擎(本例中是Calithalib.dll和GoldParserEngine.dll)加入到C#项目中,还有.net版本应不低于2.0。

      我建立了一个wpf项目,在GUI中调用解析器。代码如下:

         //读入文法表文件

         //tb2是一个文本控件,将其传入MyParser类,用于输出解析过程中的相关信息,比如错误信息和结果等等

      //MyParser的构造函数已被修改,为的是引入文本控件tb2

      MyParser parser = new MyParser(new FileStream("./Cal.cgt",FileMode.Open),tb2);

         //tb1是一个文本输入控件,tb1.Text表示输入源码

      parser.Parse(tb1.Text);

     

    5. 如何添加解释执行代码

     

      5.1 自定义类型

      对于源语言,通过语法分析和词法分析,在源语言被正确解析后,所有的工作就可以看成语法树规约的过程。在词法分析和语法规约的过程中,对于每一个树节点都可以构造一个对象。这就需要我们自己来构造相应的类。

      对于加减法器的文法,需要构造的类有两个,分别针对产生式中的Expression、Numeric符号。

        class Expression
        {
            public Location_ loc;
            public int e_value;//存储产生式规约时所计算的表达式的值
            public Expression(int e_value, int lineNr, int columnNr) {}
            public Expression(int e_value, Location_ loc) {}
        }
        class Numeric
        {
            public Location_ loc;
            public int n_value;//存储产生式规约时的数字的值
            public Numeric(int n_value, int lineNr, int columnNr {}
            public Numeric(int n_value, Location_ loc){}
        }

     

       5.2 添加词法分析时的对象构造代码:

      词法分析中,主要修改MyParser类中的函数为:public static Object CreateObject(NonterminalToken token)

      该函数终结符识别对应的事件处理函数TokenReadEvent所调用。从CreateObject函数的定义可以看出,我们只需要对相应的终结符返回对象就可以了,加法器文法中只需要针对数字串返回Numeric对象。在CreateObject中添加如下代码:

            private Object CreateObject(TerminalToken token)
            {
                switch (token.Symbol.Id)
                {
          ……

                    case (int)SymbolConstants.SYMBOL_NUMBER :
                    //Number
                    //todo: Create a new object that corresponds to the symbol
                    //return null;
                    return new Numeric( Convert.ToInt32(token.Text),token.Location.LineNr,token.Location.ColumnNr);

          ……

                }
                throw new SymbolException("Unknown symbol");
            }

     

      5.3 添加语法分析时的对象构造代码:

      词法分析中,主要修改MyParser类中的函数为:public static Object  CreateObject(TerminalToken token)

      该函数被语法规约所对应的事件处理函数ReduceEvent所调用。从CreateObject函数的定义可以看出,我们只需要对相应的产生式返回对象就可以了。在CreateObject中添加如下代码:

            public static Object CreateObject(NonterminalToken token)
            {
                switch (token.Rule.Id)
                {
                    case (int)RuleConstants.RULE_EXPRESSION_PLUS_NUMBER:
                        {
                            Expression a_plus = token.Tokens[0].UserObject as Expression;
                            Numeric b_plus = token.Tokens[2].UserObject as Numeric;
                                return new Expression(a_plus.e_value + b_plus.n_value, a_plus.loc);
                        }
                    case (int)RuleConstants.RULE_EXPRESSION_MINUS_NUMBER :
                        {
                            Expression a_minus = token.Tokens[0].UserObject as Expression;
                            Numeric b_minus = token.Tokens[2].UserObject as Numeric;
                                return new Expression(a_minus.e_value - b_minus.n_value, a_minus.loc);
                        }
                    case (int)RuleConstants.RULE_EXPRESSION_NUMBER :
                        return new Expression(((Numeric)(token.Tokens[0].UserObject)).n_value,
                                                (token.Tokens[0].UserObject as Numeric).loc);
                }
                throw new RuleException("Unknown rule");
            }

     

      5.4 解析完成事件——执行结果

      在以上代码添加完成后,这时,语法解释器就已经基本完成了,它可以读入正确加法器语言,输出结果。结果在哪能看到呢?

      加法器语言被正确解析后,事件处理函数AcceptEvent被调用,在该函数中可以添加输出结果代码

      private void AcceptEvent(LALRParser parser, AcceptEventArgs args)
          {

        //ouputTB是传入到MyParser的文本控件

        //args.Token.UserObject是产生式起始符号的所对应的用户对象。

        outputTB.Text = (args.Token.UserObject as Expression).e_value.ToString();

      }

      看看输出结果截图:)

      能看到结果了,可是还有事情要做:词法错误识别、语法错误识别、语义错误。So many……God save me!
     

    6. 错误提示功能实现:

     

      6.1 词法错误提示:

         在自动生成的词法错误处理函数TokenErrorEvent中添加错误提示代码:

            private void TokenErrorEvent(LALRParser parser, TokenErrorEventArgs args)
            {
                //string message = "Token error with input: '"+args.Token.ToString()+"'";
                string position = "Location Line:"+args.Token.Location.LineNr+" Column:" + args.Token.Location.ColumnNr+"   ";
                string message = "Token error with input: '" + args.Token.ToString() + "'";
                //输出到GUI
                outputTB.Text = position + message;
            }

     

      6.2 语法错误提示:

         在自动生成的语法错误处理函数ParseErrorEvent中添加错误提示代码:

            private void ParseErrorEvent(LALRParser parser, ParseErrorEventArgs args)
            {
                string position = "Location Line:" + args.UnexpectedToken.Location.LineNr + " Column:" +                                                             args.UnexpectedToken.Location.ColumnNr + "   ";
                string message = "Parse error caused by token: '"+args.UnexpectedToken.ToString()+"'";
                //输出到GUI
                outputTB.Text = position+message;
            }

     

      6.3 一个小扩展——语义错误提示:

      关于语义分析,在自动生成的骨架代码中是没有的。恐怕任何一个语法分析工具都不可能替用户实现这种功能。语言符号终究只是符号,它的实际含义需要用户去定义。比如3”+“0,常识上说应该没有任何语义问题,然而”+“号在另一种语言中的实际含义也许就相当于四则运算中的除法,这是就出现问题了,分母为0。同一种文法表示的语言在不同的人眼中意义是不一样的,就像今天的骨感美女在唐朝也许就是绝对的丑女一样。因此语义错误只能由用户定义,无法由工具代劳。

      在加减法器中,有几个语义上的规则需要满足:

    • 数字不能超过32位整数的范围
    • 数字之和不能大于32位整数的最大值
    • 数字之差不能小于32位整数的最小值

      为了确认错误的地址,需要保存错误的地址,于是创建Location_类,并为Numeric和Express类添加Location_属性。

      为了检测计算是否超过32位整数范围,添加检测函数

    //检查加法运算是否会超出范围

            private static Boolean CheckSUM(int a, int b){}

    //检查减法运算是否会超出范围

            private static Boolean CheckDIFF(int a, int b){}

      修改CreateObject(NonterminalToken token)函数中的规约代码,添加语义分析代码:

                    case (int)RuleConstants.RULE_EXPRESSION_PLUS_NUMBER:
                        //<expression> ::= <expression> '+' Number
                        {
                            Expression a_plus = token.Tokens[0].UserObject as Expression;
                            Numeric b_plus = token.Tokens[2].UserObject as Numeric;
                            if (CheckSUM(a_plus.e_value, b_plus.n_value))
                                return new Expression(a_plus.e_value + b_plus.n_value, a_plus.loc);
                            else
                                throw new Exception("the sum is too large or too small!");
                        }
                    case (int)RuleConstants.RULE_EXPRESSION_MINUS_NUMBER :
                    //<expression> ::= <expression> '-' Number
                        {
                            Expression a_minus = token.Tokens[0].UserObject as Expression;
                            Numeric b_minus = token.Tokens[2].UserObject as Numeric;
                            if (CheckDIFF(a_minus.e_value, b_minus.n_value))
                                return new Expression(a_minus.e_value - b_minus.n_value, a_minus.loc);
                            else
                                throw new Exception("the difference is too large or too small!");
                        }

      现在一切ok了。能查出语义错误了。

     

    7. 平凡小节

      本文实现一个正整数的加减法器,实现了解释执行、词法错误提示、语法错误提示、语义错误提示等功能。

      本例用C#实现,然而其意义应不止于此。如果是解释器,用C#开发可能执行慢一点,然而相比C#平台丰富的支持来说,应该值得;如果是编译器,由于任何一种语言可以用来开发编译器的前端,C#当然也在其中,所影响的只是编译的速度而已,对于编译出来的最终目标程序来说,执行速度没有差别。

        本文几乎覆盖了编译器前端的所有基本概念。

      到这里有人可能要说了,这样一个简单的加减法器,很多概念都没有提到,比如分支语言、循环语句号,比如符号表,比如函数嵌套。

      首先,分支语句、循环语句等过程语句只是控制语句中的一部分,跟加法器语句没有本质上不同。所不同的是,各种过程语句有它自己的特点,有着各自的解决技巧,比如分支语句就可以用栈来存储条件判断语句的值,这里就不细究了。

      其次,函数嵌套,涉及到参数传递,用堆栈传值等等可以解决。

      再次,符号表也涉及到不少编译的知识,比如作用域嵌套等等。

      但是,总的来说,我认为上面那几点都只是单纯的技巧,拿本编译课本翻翻就好了。

      还有,本文的目的还是介绍语法分析工具Gold的运用,语法分析生成工具没有提供自动语义分析的义务,也提供不了。本文中已经介绍了一种语义错误提示功能。所有复杂的语义,归根结底,都只是华丽的技巧,并不是高不可攀。只要了解了语义分析的实质,在课本的帮助下,一切复杂语义都是纸老虎,尽管这纸老虎还很不好折。

      就简单的编译器和解释器来说,本文介绍的知识和方法应该够用了。如果要做一个复杂的编译器或解释器,比如C语言的编译器,那你还是去翻课本吧。

      最后还是向大家推荐一下语法分析工具Gold,自动生成的代码的可读性太好了。其实我写这篇文章甚至比我学习Gold的时间还长……

     源码下载:/Files/sword03/Cal_Parser.rar

  • 相关阅读:
    一点一点学ASP.NET之基础概念——HttpModule
    Repeater为空时显示处理
    数据绑定控件的ItemDataBound事件与System.Data.Common.DbDataRecord
    Fckeditor编辑器添加【行距】
    gridview用法大全(二)
    Web.config自定义节点configSections
    C#中的特性(Attributes)(翻译)
    给程序员的忠告
    项目管理(转载)
    在ASP.NET 2.0中使用样式、主题和皮肤(转载)
  • 原文地址:https://www.cnblogs.com/ljl_falcon/p/2513150.html
Copyright © 2011-2022 走看看