• 《Language Implementation Patterns》之 增强解析模式


    上一章节讲述了基本的语言解析模式,LL(k)足以应付大多数的任务,但是对一些复杂的语言仍然显得不足,已付出更多的复杂度、和运行时效率为代价,我们可以得到能力更强的Parser。

    • Pattern 5 :回朔解析器(Backtracking Parser),这种解析器晖尝试规则的每个分支来进行匹配,与LL(k)比较的话,Backtracking Parser支持任意长度的预读token,这种Parser的能力极强,运行时的代价可能会很大。
    • Pattern 6 :Memoizing Parser, 这中parser通过一些内存消耗来调高parse效率;
    • Pattern 7 : Predicated Parser, 允许我们通过boolean表达式来调整parser的控制流程,前面所讲的任何一中模式都可以通过Predicate(谓词)来扩展。

    这几个模式非常的繁琐,一般通过工具来生成,但是弄清楚原理才能理解工具所生成的Parser。

    为什么需要回朔

    有些语言通过LL(k)不能实现Parser,比如下面的C++语句:

    void bar() {...}
    void bar();
    

    对应的语法规则类似:

    function : def | decl ;
    def :functionHead '{' body '}' ;
    decl : functionHead ';'
    functionHead : ...; //E.g., "int * (*foo)(int *f[], float)"
    

    由于functionHead的长度是不可预料的,所有LL(k)在这里不适用,只有每个选择项都尝试Parse才能得到正确结果:

    void function() {
       if ( «speculatively-match-def» ) def();
       else if ( «speculatively-match-decl» ) decl();
        else throw new RecognitionError("expecting function");
    }
    

    上面这个Parser有一个微妙的地方,if-else里面对规则选项的安排顺序,决定了各选项的优先级。这个特性可以用来解决C++语言一些模糊的规则,比如T(a)既可以是一个函数声明也可以是一个表达式,c++参考手册说明应该“函数声明”优先。

    回朔有两个明显的缺点:1、调试比较困难,回朔的路径很多,层次很深;2、速度慢。

    在Parser找到匹配的规则之前,同一个子规则可能被同一个输入匹配多次。比如上文所说的函数定义和函数声明,二者开始的部分完全相同,回朔法对functionHead用同样的输入发生两次匹配。假如在尝试规则def的时候能够记住functionHead匹配情况,那么对decl的尝试就能更快,Pattern 6 Memoizing Parser解释了这个机制。

    上下文相关文法

    上面的parser设计都是用来对付上下文无关语言的,“上下文无关”的意思规则的匹配不依赖与具体的语句上下文。应该说,大部分编程语言都是上下文无关的,但是这些语言的某些规则却存在“上下文相关性”。

    看一个例子,T(6)在C++里面可能是一个函数调用,也可能是一个对象构造,取决于T是一个函数还是一个类名。

    expr: INTEGER // integer literal
           | ID '(' expr ')' // function call; AMBIGUOUS WITH NEXT ALT
           | ID '(' expr ')' // constructor-style typecast
           ;
    

    这个语法规则描述了函数与对象构造,但是如果按照之前的模式来编写Parser,第三个选项永远不会被匹配。

    为了让上下文无关的Parser能够处理这样的语言,需要对规则选项增加谓词(Predicate)。谓词是一个运行时的boolean条件,当条件为真时,某个选项有效,Parser方法应该这样编写:

    void expr() {
        if ( LA(1)==INTEGER) match(INTEGER);
        else if ( LA(1)==ID && isFunction(LT(1).text) ) «match-function-call» 
        else if ( LA(1)==ID && isType(LT(1).text) )     «match-typecast»
        else «error»
    }
    

    Pattern 5 Backtracking Parser

    实现回朔Parser需要一种更复杂结构,这一节描述了如何实现一个Backtracking Parser。
    这种Parser的规则对应方法模板如下:

    public void «rule»() throws RecognitionException {
         if ( speculate_«alt1»() ) { // attempt alt 1
             «match-alt1 » 
          }
         else if ( speculate_«alt2»() ) { // attempt alt 2
              «match-alt2 »
          }
          ...
           else if ( speculate_«altN»() ) { // attempt alt N
              «match-altN »
           }
           // must be an error; no alternatives matched
           else throw new NoViableException("expecting «rule»") 
    }
    

    speculate_Alt方首先为token流做一个标记,然后尝试匹配,最后无论匹配是否成功都将token流回朔到初始位置:

    public boolean speculate_«alt»() {
         boolean success = true;
         mark(); // mark this spot in input so we can rewind
         try { «match-alt» } // attempt to match the alternative
         catch (RecognitionException e) { success = false; }
         release(); // either way, rewind to where we were before attempt return success;
    }
    

    token流的mark()操作,基于一个栈结构,进入更深一层时push一个mark,回退时pop一个mark。

    Pattern 6 Memoizing Parser

    又被称之为Packrat parser(具体意思不清楚),避免对同一个规则、同一输入做重复的匹配尝试。
    以下面的语法为例:

    s : expr '!' // assume backtracking parser tries this alternative 
       | expr ';' // and then this one
       ;
    expr : ... ; // match input such as "(3+4)"
    

    在解析语句(3+4);的时候,先使用规则s的第一个选项,在最后一个符号;会失败,导致回朔;然后使用s的第二个选项,又要冲洗匹配一次expr。如果在第一个选项匹配之后,能够知道expr是否曾经匹配成功,如果成功在那个位置,那么在第二个选项的匹配时,无论如何expr可以直接跳过。

    为了记住尝试匹配的中间结果,需要一个字典型的结构{rule:condition},condition记录了一个rule的匹配状态,可能的值:unknow,failed,succeeded。如果是java语言实现的parser,那么unknow用默认null表示,failed用负数表示,succeeded用0或正数来表示(同时可以表示匹配的位置),parser方法的模板如下:

    Map<Integer, Integer> «rule»_memo = new HashMap<Integer, Integer>();
    public void «rule»() throws RecognitionException {
        boolean failed = false;
        int startTokenIndex = index();
       if ( isSpeculating() && alreadyParsedRule(«rule»_memo) ) return; 
       // must not have previously parsed rule at token index; parse it 
       try { _«rule»(); }
       catch (RecognitionException re) { failed = true; throw re; } 
       finally {
          // succeed or fail, we must record result if backtracking
          if (isSpeculating())
             memoize(«rule»_memo, startTokenIndex, failed);
       }
    }
    
    

    原来的匹配方法改名为 _«rule»(加了一个下划线),而«rule»()加上了记录中间匹配结果的逻辑,在尝试匹配结束后,执行正式的匹配的时候,就可以clear这个中间结果了。
    这个方法是对每个rule简历一个map来存储中间匹配位置,确实在一次尝试里面,一个rule可以发生多次匹配。在clear的时候,需要清楚所有rule的map。

    Pattern 7 Predicated Parser

    语法谓词(semantic predicate)用来帮助Parser做决策,最常见的情况,parser需要使用符号表里面的信息来引导接下来的解析。
    下面是加了谓词的解析方法:

    public void «rule»() throws RecognitionException {
       if ( «lookahead-test-alt1» && «pred1» ) { // attempt alt 1
          «match-alt1 »
        }
       else if ( «lookahead-test-alt2» && «pred2» ) { // attempt alt 2
           «match-alt2 »
       }
       ...
       else if ( «lookahead-test-altN» && «predN» ) { // attempt alt N
          «match-altN » 
       }
        // must be an error; no alternatives matched
       else throw new NoViableException("expecting «rule»") 
    }
    
    

    以上文C++函数调用&对象构造的问题为例,与方法规则可以如下定义:

    expr: INTEGER // integer literal
           | {isFuncName(LT(1).getText())}? ID '(' expr ')' // function call; AMBIGUOUS WITH NEXT ALT
           | {isTypeName(LT(1).getText())}? ID '(' expr ')' // constructor-style typecast
           ;
    

    LT(1)代表往前预读的第一个token。

  • 相关阅读:
    第0课
    学前班-怎么看原理图
    LCD-裸机韦东山
    学前班
    专题8-Linux系统调用
    专题4-嵌入式文件系统
    网络编程 之 软件开发架构,OSI七层协议
    反射、元类,和项目生命周期
    多态、魔法函数、和一些方法的实现原理
    封装,接口,抽象
  • 原文地址:https://www.cnblogs.com/longhuihu/p/4000297.html
Copyright © 2020-2023  润新知