zoukankan      html  css  js  c++  java
  • VSX开发之语言服务系列(9)——插曲MPPG

    前言

    本文旨在深入详解MPPG,掌握它对于编写parser是必须的。本文的行文思路是根据SDK额外文档MPPG.pdf中的内容,可以在如下路径下找到该文档:C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\ExtraDocumentation。本文适合那些对parser.y一知半解的读者阅读。

    概述

    MPPG(Managed Package Parser Generator)是一个将具有YACC语法风格的程序编译生成C#目标程序的代码生成器。它是微软用来在VSX开发中,取代yacc和bison的工具,并且在某些方面跟Babel有一定的耦合。由于生成器和运行时完全是基于C#的,而且广泛使用了泛型集合类,所以需要.NET 2.0的支持。

    MPPG的运行

    MPPG需要在命令行中执行。当然,我们在工程中会为.y文件设置MPPGCompiler,于是在编译工程前,IDE会为我们自动调用类似如下命令行,以启动MPPG编译:

    C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Tools\bin\MPPG.exe" -mplex "Generated\parser.y" > "obj\Debug\parser.cs

    尽管如此,我还是建议大家学会手动在命令行中调用。因为当.y文件本身有语法错误时(这种情况在最初学的时候十分常见),会造成编译失败,但是IDE不能捕获这种错误,最多就是抛出一些毫不相关的错误,开发人员对错误莫名其妙。但是在命令行中错误的详细信息会打印出来。我见的最多的就是像下面的问题:

    Error    1    The type or namespace name 'ScanBase' could not be found

    这个错误通常是.y文件的语法错误。

    另外,在命令行中可以指定一些命令行参数,但是不怎么常用:/help 显示帮助信息、/version 显示版本信息、/report 显示 LALR(1) 状态 信息、/no-lines 输出代码不包含#line。

    使用MPPG生成的Parser

    MPPG生成的Parser为用户提供了简单的接口。用户可以在自己的代码中实例化Parser,Parser的类名总是Parser。很常见的情况是,将一个扫描器(scanner)和一个错误处理(error handler)对象与Parser实例联系在一起,然后扫描器和输入文本联系在一起。

    调用解析器实例的Parse()方法,这个方法继承自抽象类ShiftReduceParser,方法的原型如下:

    public bool Parse() { ... }

    如果解析成功的话,这个方法会返回true,反之,则返回false。解析成功与否取决于在解析过程中是否检测到错误,False暗示了解析过程异常退出。

    通常,解析器不仅仅是要返回解析成功与否。在大多数情况下,还希望解析器顺便构造一个语法树或是一个符号表,这些结果通常会作为parser内部的可访问字段,以便其他的程序检索。

    MPPG输出

    MPPG读入一个语法定义文件(通常是.y文件),输出一个C#文件(通常是对应的.cs文件),输出文件包括一下内容:

    * 一个标记类型的枚举

    public enum Tokens {error=127, EOF=128, ... }

    枚举值总是大于在语法定义文件中出现过的任何字符值。

    * 在语法定义中的“union”类型被定义成的结构

    public partial struct ValType{ ... }

    这个结构称为语义值类型,这个类由扫描器的yylval返回。这个类型相当于传统的yacc中所说的YYSTYPE类型。如果在*.y文件中出现%partial声明,那么这个结构将是部分的。

    * 实现解析器功能的类

    public partial class Parser : ShiftReduceParser<ValType, LocType> {
    
    ...
    
    }

    如果在*.y文件中出现%partial声明,那么这个类型将是部分的。这个类型给定了基类ShiftReduceParser中的泛型ValType, LocType,这两个类型可以在语法定义文件中声明,MPPG能够将声明转化成相应的ValType, LocType。(注:在语法定义文件中,%valuetype定义的只是ValType的名字,MPPG总是将y文件中的union编译成以ValType命名的结构,并认为ValType这个泛型就是这个结构;对于LocType,默认情况下MPPG包含一个Babel.ParserGenerator的名字空间,并查找其中实现IMerge<YYLTYPE>接口的类型,将其作为LocType,在Babel的IScanner.cs中便有这样一个类实现了这个接口:public class LexLocation : IMerge<LexLocation>。如果要在y中指定LocType需要%YYLTYPE声明,否则将取默认行为)

    生成的C#源文件,除了包含上述的类型,还包含一个解析用的解析表,以及用户定义的语义动作代码。解析器使用的是“自下而上的LALR(1)”移进-规约算法,这依赖于运行时组件MppgRuntime.dll

    扫描器接口

    public abstract class AScanner<YYSTYPE, YYLTYPE>
    where YYSTYPE : struct
    where YYLTYPE : IMerge<YYLTYPE>
    {
    	public YYSTYPE yylval;
    	public YYLTYPE yylloc;
    	public abstract int yylex();
    	public virtual void yyerror(string msg,
    	param object[] args) {}
    }

    解析器的实例包含一个共有的字段scanner。Mppg需要引用这个实现了上面抽象类的对象实例。AScanner是个扫描器的抽象基类,这个基类定义了mppg运行时扫描器需要的接口。当然除此之外,通常扫描器还要实现其他语义行为需要的功能,这些行为需要更丰富的接口,但是对于解析引擎本身需要且只需要这样的接口就够了。

    上面的代码是个带有两个泛型参数的类,第一个,YYSTYPE是标记的语义值类型。如果语法没有定义语义值类型,默认将是int。第二个,YYLTYPE是个标记的位置类型,用来跟踪将要被解析的文本的位置。对于大多数的应用,用默认的LexLocation类型足够了:

    public class LexLocation : IMerge<LexLocation>

    AScanner定义了两个字段,用来在扫描器和解析器之间传递语义值和位置值,yylvalyyllocyylex()返回对应下一个标记的数值,这是个抽象方法,扫描器必须重写它。yyerror()是个最底层的错误处理函数,在解析器错误恢复时被调用。扫描器可以有选择性的重写这个方法。另外语义动作可以明确地搜集这些错误信息,可以在解析中利用位置信息搜集,而不用yyerror,错误处理将在下面介绍。

    public abstract class ScanBase : AScanner<int,LexLocation>, IColorScan
    {
    	protected int currentScOrd;
    	public virtual int GetEolState() { return currentScOrd; }
    	public virtual void SetEolState(int value) { currentScOrd = value; }
    	public abstract void SetSource(string s, int o);
    	public abstract int GetNext(ref int state, out int start, out int end);
    }

    如上面的代码为了方便,parser文件还定义了一个抽象的包装类(wrapper class)封装AScanner和IColorScan接口(实际上在我所使用的MPPG和MPLex版本中,MPPG没有封装,而是由MPLex自行封装的,因此上面的代码需要在lexer.cs中才能找到),用来对扫描的标记找色。用这个类,scanner可以避免直接实现AScanner而是扩展ScanBase类,一个典型的例子如下,SetSource用来关联scanner的输入字符串缓冲,可以传入完整的字符串,也可以更具应用不同逐行传入,整形参数是开始偏移量,有了这个参数scanner就不必每次从第一个字符开始了。

    MPPG输入

    Mppg的输入语法是基于传统的YACC语言的。目前的版本仍然有些没有实现的结构,在对C#编程语言方面却有一些小的扩展。

    Mppg会对合法的输入语法做一些小的检查。如果特定的符号没有出现在标记声明段中,并且也不是任何终结符,那么语法就是非终结的。Mppg将会产生错误信息:the symbol that is involved。在终结测试中,对于不可达的非终结符,会产生警告信息,不过不是致命的错误。语法的排版错误是引起上述错误最多的原因。在mppg中符号定义是区分大小写的。

    未能实现的结构和限制

    Mppg目前不支持Bison中“%expect N”的表达。语义行为中只能引用右侧前9个符号,也就是说”$n”和”@n”中的n只能是单数0-9。

    扩展的语法

    语义值类型的定义

    传统的扫描器期望的语义值类型是YYSTYPE,是在语法定义文件中用union结构定义的。然而C#不支持这种联合体类型,只能通过继承实现相同的功能。Mppg能够识别“%union”结构的表述,在输出文件中相应的定义成struct。Struct会包含union中定义的所有字段。如果不设置%valuetype,缺省情况下Union类型的命名是ValueType。

    %partial

    这个标记定义在开头,声明生成的parser类是个部分类,通过这个机制,可以方便的在其他文件中定义非语义的代码。如果定义了部分类,那么由union生成的struct也是部分的。这种部分类的机制的确很有用,可以在语法文件中仅定义语法和行为,大量的实现代码和复杂的逻辑都可以在分开的文件中定义;不仅如此,语义值类型 的字段甚至方法实现也可以定义在分开的文件中,否则的话不得不把方法的实现部分写在union中。Babel中的Parser.cs就是这种机制的直接受益者,因此,永远不要忘了声明%partial。

    %namespace NameSpaceName

    Mppg的输出文件被包含在这里定义的名字空间下,可以用”.”号。这里最好永远是%namespace Babel.Parser。

    %YYSTYPE ValueTypeName

    该标记与“%valuetype”相同。

    %YYLTYPE LocationTypeName

    这个标记可以覆盖掉默认的位置类型值的名字,LexLocation。在大多数应用中,默认的类型已经够了, 但是如果需要一些额外的功能,建议重写一个新的类型,并用这个标记命名这个类型。

    %using UsingName

    声明引用的名字空间。通常需要包含using Microsoft.VisualStudio.TextManager.Interop,另外using Babel.ParserGenerator也是必须的,但是MPPG默认会在输出中包括这个名字空间,因为他与生俱来需要跟Babel“合作”。

    parser动作

    在解析的过程中即不能移进又不能规约时,默认的行为是调用scanner接口的yyerror方法。此时运行时将丢弃状态值、值、位置栈直到可以找到一个叫”error”的标记被移进。在”error”标记移进后,解析器进一步查找下一个标记是否能够进行普通的移进或规约,如果失败,这个标记将被丢弃,直到找到一个可能的标记或者输入结束。理解这个原则,对于实现语法错误提示至关重要。

    语义行为

    语义行为可以针对位置值和语义值,当你要将错误信息传递给错误处理的时候,一般要用到位置值,当你要返回标记的语义时(比如简单的字串),就要用到语义值。

    位置跟踪

    Scanner的第二个泛型参数YYLTYPE是个表示位置的类型。位置类型包括标记的起始位置和终止位置信息,以表示一个文本区域,YYLTYPE必须要实现IMerge接口:

    public interface IMere<YYLTYPE> {
    	YYLTYPE Merge(YYLTYPE last);
    }

    这个接口实现一个合并的方法,表示将当前的位置与参数last的位置合并成一个位置。对于parser来说,在每次规约的时候,会自动调用这个Merge方法,默认将右侧的所有标记位置合并成结果(非终结符)标记。在一个规约中,左侧符号的位置用@$表示,右侧标记的位置分别用 @1,@2...@n表示,注意n是单数。

    在规约时,默认的行为像这样:

    @$ = @1.Merge(@N)

    这里的N是右侧标记的个数,这个行为会在任何一个用户定义的语义行为之前执行。所以用户可以为@$指定新的位置,以覆盖这种行为。

    如果scanner没有返回这种位置类型的实现,那么scanner的yylloc字段将总是null。在解析器进行默认规约行为时由于null的情况会被测试,所以这不会造成异常。不过默认的LexLocation总是实现的:

    public class LexLocation : IMerge<LexLocation>
    {
    	public int sLin; // Start line
    	public int sCol; // Start column
    	public int eLin; // End line
    	public int eCol; // End column
    	public LexLocation() {};
    	public LexLocation(int sl; int sc; int el; int ec)
    	{ sLin=sl; sCol=sc; eLin=el; eCol=ec; }
    	public LexLocation Merge(Lexlocation end) {
    		return new LexLocation(sLin,sCol,end.eLin,end.eCol);
    	}
    }

    语义跟踪

    要在语义行为中使用语义跟踪,语义值需要扫描器向解析器传递,简单的说,就是扫描器对yylval赋值,解析器从yylval中取关心的值,yylval是union中定义的YYSTYPE。在parser的一个规约中,在一个规约中,左侧符号的语义用$$表示,右侧标记的语义分别用 $1,$2...$n表示,注意n是单数。

    例如:在ManagedMyC中,YYSTYPE被定义成如下的结构:

    public partial struct LexValue
    #line 9 "GeneratorSource\parser.y"
    			{
        public string str;
    }

    于是,我们需要在lex的匹配行为中,像这样写,这里我们把标记的文本作为语义类型传递给parser

    yylval.str = yytext;

    在parser的语义行为中就可以像这样获得这个标记的字符串(如果这个标记位于一个规则的第n个的话):

    $n.str;

  • 相关阅读:
    maven继承父工程统一版本号
    shiro权限控制参考
    动态查询列表页面的分页
    SVN服务器更改ip地址后怎么办
    cookie记住密码功能
    分享小插件的问题
    阿里云短信验证
    从svn上更新maven项目时,所有文件变成包的形式
    Maven工具
    Mybatis的dao层传递单参出现的问题
  • 原文地址:https://www.cnblogs.com/P_Chou/p/1739960.html
Copyright © 2011-2022 走看看