《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。
《Language Implementation Patterns》之 增强解析模式的更多相关文章
- 《Language Implementation Patterns》之 解释器
		前面讲述了如何验证语句,这章讲述如何构建一个解释器来执行语句,解释器有两种,高级解释器直接执行语句源码或AST这样的中间结构,低级解释器执行执行字节码(更接近机器指令的形式). 高级解释器比较适合DS ... 
- 《Language Implementation Patterns》之 符号表
		前面的章节我们学会了如何解析语言.构建AST,如何访问重写AST,有了这些基础,我们可以开始进行"语义分析"了. 在分析语义的一个基本方面是要追踪"符号",符号 ... 
- 《Language Implementation Patterns》之访问&重写语法树
		每个编程的人都学习过树遍历算法,但是AST的遍历并不是开始想象的那么简单.有几个因素会影响遍历算法:1)是否拥有节点的源码:2)是否子节点的访问方式是统一的:3)ast是homogeneous或het ... 
- 《Language Implementation Patterns》之 构建语法树
		如果要解释执行或转换一段语言,那么就无法在识别语法规则的同时达到目标,只有那些简单的,比如将wiki markup转换成html的功能,可以通过一遍解析来完成,这种应用叫做 syntax-direct ... 
- 《Language Implementation Patterns》之 强类型规则
		语句的语义取决于其语法结构和相关符号:前者说明了了要"做什么",后者说明了操作"什么对象".所以即使语法结构正确的,如果被操作的对象不合法,语句也是不合法的.语 ... 
- 《Language Implementation Patterns》之 数据聚合符号表
		本章学习一种新的作用域,叫做数据聚合作用域(data aggregate scope),和其他作用域一样包含符号,并在scope tree里面占据一个位置. 区别在于:作用域之外的代码能够通过一种特殊 ... 
- 《Language Implementation Patterns》之 语言翻译器
		语言翻译器可以从一种计算机语言翻译成另外一种语言,比如一种DSL的标量乘法axb翻译成java就变成a*b:如果DSL里面有矩阵运算,就需要翻译成for循环.翻译器需要完全理解输入语言的所有结构,并选 ... 
- 【BZOJ】【1717】【USACO 2006 Dec】Milk Patterns产奶的模式
		后缀数组 o(︶︿︶)o 唉傻逼了一下,忘了把后缀数组的字典范围改回20001,直接21交了上去,白白RE了两发……sigh 既然要找出现了K次的子串嘛,那当然要用后缀数组了>_>(因为我 ... 
- BZOJ 1717: [Usaco2006 Dec]Milk Patterns 产奶的模式 [后缀数组]
		1717: [Usaco2006 Dec]Milk Patterns 产奶的模式 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 1017 Solved: ... 
随机推荐
- 数据分析神器Colab的初探
			为什么要使用Colab 使用过Jupyter(参看<「极客时间」带来的社区价值思考>章节:社区交流的基建设施)的朋友,一定会醉心于它干净简洁的设计,以及在"摆脱Python命令行 ... 
- 爬虫工具fiddle在firefox浏览器中的使用
			1.fiddle工作原理 浏览器与服务器之间通过建立TCP连接以HTTP协议进行通信,默认通过自己发送HTTP(或HTTPS)请求到服务器. 使用Fiddler之后,浏览器像目标服务器发送的请求都会经 ... 
- 【BZOJ4009】接水果(整体二分,扫描线)
			[BZOJ4009]接水果(整体二分,扫描线) 题面 为什么这都是权限题???,洛谷真良心 题解 看到这道题,感觉就是主席树/整体二分之类的东西 (因为要求第\(k\)大) 但是,读完题目之后,我们发 ... 
- 【洛谷1131】【ZJOI2007】时态同步
			题面 题目描述 小Q在电子工艺实习课上学习焊接电路板.一块电路板由若干个元件组成,我们不妨称之为节点,并将其用数字1,2,3-.进行标号.电路板的各个节点由若干不相交的导线相连接,且对于电路板的任何两 ... 
- [Luogu3377]【模板】左偏树(可并堆)
			题面戳我 题目描述 如题,一开始有N个小根堆,每个堆包含且仅包含一个数.接下来需要支持两种操作: 操作1: 1 x y 将第x个数和第y个数所在的小根堆合并(若第x或第y个数已经被删除或第x和第y个数 ... 
- Python模块之hashlib模块、logging模块
			一.hashlib模块 hashlib模块介绍:hashlib这个模块提供了摘要算法,例如 MD5.hsa1 摘要算法又称为哈希算法,它是通过一个函数,把任意长度的数据转换为一个长度固定的数据串,这个 ... 
- Android 视频展示控件之 SurfaceView、GLSurfaceView、SurfaceTexture、TextureView 对比总结
			一.SurfaceView SurfaceView继承自View,并提供了一个独立的绘图层,你可以完全控制这个绘图层,比如说设定它的大小,所以SurfaceView可以嵌入到View结构树中,需要注意 ... 
- Redis进阶实践之十八 使用管道模式加速Redis查询
			一.引言 学习redis 也有一段时间了,该接触的也差不多了.后来有一天,以为同事问我,如何向redis中批量的增加数据,肯定是大批量的,为了这主题,我从新找起了解决方案.目前 ... 
- 精解Mat类(一):基本数据类型-固定大小的 矩阵类(Matx) 向量类(Vector)
			一.基础数据类型 1.(基础)固定大小矩阵类 matx 说明: ① 基础矩阵是我个人增加的描述,相对于Mat矩阵类(存储图像信息的大矩阵)而言. ② 固定大小矩阵类必须在编译期间就知晓其维 ... 
- 电路ppt作业
			例二 例三 3.例一 
