💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### 6.6 处理二义文法 上面例子中,对表达式的文法描述用一种特别的形式规避了二义文法。然而,在很多情况下,这样的特殊文法很难写,或者很别扭。一个更为自然和舒服的语法表达应该是这样的: ~~~ expression : expression PLUS expression | expression MINUS expression | expression TIMES expression | expression DIVIDE expression | LPAREN expression RPAREN | NUMBER ~~~ 不幸的是,这样的文法是存在二义性的。举个例子,如果你要解析字符串”3 * 4 + 5”,操作符如何分组并没有指明,究竟是表示”(3 * 4) + 5”还是”3 * (4 + 5)”呢? 如果在yacc.py中存在二义文法,会输出”移进归约冲突”或者”归约归约冲突”。在分析器无法确定是将下一个符号移进栈还是将当前栈中的符号归约时会产生移进归约冲突。例如,对于”3 * 4 + 5”,分析器内部栈是这样工作的: ~~~ Step Symbol Stack Input Tokens Action ---- --------------------- --------------------- ------------------------------- 1 $ 3 * 4 + 5$ Shift 3 2 $ 3 * 4 + 5$ Reduce : expression : NUMBER 3 $ expr * 4 + 5$ Shift * 4 $ expr * 4 + 5$ Shift 4 5 $ expr * 4 + 5$ Reduce: expression : NUMBER 6 $ expr * expr + 5$ SHIFT/REDUCE CONFLICT ???? ~~~ 在这个例子中,当分析器来到第6步的时候,有两种选择:一是按照expr : expr * expr归约,一是将标记’+’继续移进栈。两种选择对于上面的上下文无关文法而言都是合法的。 默认情况下,所有的移进归约冲突会倾向于使用移进来处理。因此,对于上面的例子,分析器总是会将’+’进栈,而不是做归约。虽然在很多情况下,这个策略是合适的(像”if-then”和”if-then-else”),但这对于算术表达式是不够的。事实上,对于上面的例子,将’+’进栈是完全错误的,应当先将expr * expr归约,因为乘法的优先级要高于加法。 为了解决二义文法,尤其是对表达式文法,yacc.py允许为标记单独指定优先级和结合性。需要像下面这样增加一个precedence变量: ~~~ precedence = ( ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'), ) ~~~ 这样的定义说明PLUS/MINUS标记具有相同的优先级和左结合性,TIMES/DIVIDE具有相同的优先级和左结合性。在precedence声明中,标记的优先级从低到高。因此,这个声明表明TIMES/DIVIDE(他们较晚加入precedence)的优先级高于PLUS/MINUS。 由于为标记添加了数字表示的优先级和结合性的属性,所以,对于上面的例子,将会得到: ~~~ PLUS : level = 1, assoc = 'left' MINUS : level = 1, assoc = 'left' TIMES : level = 2, assoc = 'left' DIVIDE : level = 2, assoc = 'left' ~~~ 随后这些值被附加到语法规则的优先级和结合性属性上,这些值由最右边的终结符的优先级和结合性决定: ~~~ expression : expression PLUS expression # level = 1, left | expression MINUS expression # level = 1, left | expression TIMES expression # level = 2, left | expression DIVIDE expression # level = 2, left | LPAREN expression RPAREN # level = None (not specified) | NUMBER # level = None (not specified) ~~~ 当出现移进归约冲突时,分析器生成器根据下面的规则解决二义文法: 1. 如果当前的标记的优先级高于栈顶规则的优先级,移进当前标记 2. 如果栈顶规则的优先级更高,进行归约 3. 如果当前的标记与栈顶规则的优先级相同,如果标记是左结合的,则归约,否则,如果是右结合的则移进 4. 如果没有优先级可以参考,默认对于移进归约冲突执行移进 比如,当解析到”expression PLUS expression”这个语法时,下一个标记是TIMES,此时将执行移进,因为TIMES具有比PLUS更高的优先级;当解析到”expression TIMES expression”,下一个标记是PLUS,此时将执行归约,因为PLUS的优先级低于TIMES。 如果在使用前三种技术解决已经归约冲突后,yacc.py将不会报告语法中的冲突或者错误(不过,会在parser.out这个调试文件中输出一些信息) 使用precedence指定优先级的技术会带来一个问题,有时运算符的优先级需要基于上下文。例如,考虑”3 + 4 * -5”中的一元的’-‘。数学上讲,一元运算符应当拥有较高的优先级。然而,在我们的precedence定义中,MINUS的优先级却低于TIMES。为了解决这个问题,precedene规则中可以包含”虚拟标记”: ~~~ precedence = ( ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'), ('right', 'UMINUS'), # Unary minus operator ) ~~~ 在语法文件中,我们可以这么表示一元算符: ~~~ def p_expr_uminus(p): 'expression : MINUS expression %prec UMINUS' p[0] = -p[2] ~~~ 在这个例子中,%prec UMINUS覆盖了默认的优先级(MINUS的优先级),将UMINUS指代的优先级应用在该语法规则上。 起初,UMINUS标记的例子会让人感到困惑。UMINUS既不是输入的标记也不是语法规则,你应当将其看成precedence表中的特殊的占位符。当你使用%prec宏时,你是在告诉yacc,你希望表达式使用这个占位符所表示的优先级,而不是正常的优先级。 还可以在precedence表中指定”非关联”。这表明你不希望链式运算符。比如,假如你希望支持比较运算符’’,但是你不希望支持 a < b < c,只要简单指定规则如下: ~~~ precedence = ( ('nonassoc', 'LESSTHAN', 'GREATERTHAN'), # Nonassociative operators ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'), ('right', 'UMINUS'), # Unary minus operator ) ~~~ 此时,当输入形如 a < b < c时,将产生语法错误,却不影响形如 a < b 的表达式。 对于给定的符号集,存在多种语法规则可以匹配时会产生归约/归约冲突。这样的冲突往往很严重,而且总是通过匹配最早出现的语法规则来解决。归约/归约冲突几乎总是相同的符号集合具有不同的规则可以匹配,而在这一点上无法抉择,比如: ~~~ assignment : ID EQUALS NUMBER | ID EQUALS expression expression : expression PLUS expression | expression MINUS expression | expression TIMES expression | expression DIVIDE expression | LPAREN expression RPAREN | NUMBER ~~~ 这个例子中,对于下面这两条规则将产生归约/归约冲突: ~~~ assignment : ID EQUALS NUMBER expression : NUMBER ~~~ 比如,对于”a = 5”,分析器不知道应当按照assignment : ID EQUALS NUMBER归约,还是先将5归约成expression,再归约成assignment : ID EQUALS expression。 应当指出的是,只是简单的查看语法规则是很难减少归约/归约冲突。如果出现归约/归约冲突,yacc()会帮助打印出警告信息: ~~~ WARNING: 1 reduce/reduce conflict WARNING: reduce/reduce conflict in state 15 resolved using rule (assignment -> ID EQUALS NUMBER) WARNING: rejected rule (expression -> NUMBER) ~~~ 上面的信息标识出了冲突的两条规则,但是,并无法指出究竟在什么情况下会出现这样的状态。想要发现问题,你可能需要结合语法规则和`parser.out`调试文件的内容。 ### 6.7 parser.out调试文件 使用LR分析算法跟踪移进/归约冲突和归约/归约冲突是件乐在其中的事。为了辅助调试,yacc.py在生成分析表时会创建出一个调试文件叫parser.out: ~~~ Unused terminals: Grammar Rule 1 expression -> expression PLUS expression Rule 2 expression -> expression MINUS expression Rule 3 expression -> expression TIMES expression Rule 4 expression -> expression DIVIDE expression Rule 5 expression -> NUMBER Rule 6 expression -> LPAREN expression RPAREN Terminals, with rules where they appear TIMES : 3 error : MINUS : 2 RPAREN : 6 LPAREN : 6 DIVIDE : 4 PLUS : 1 NUMBER : 5 Nonterminals, with rules where they appear expression : 1 1 2 2 3 3 4 4 6 0 Parsing method: LALR state 0 S' -> . expression expression -> . expression PLUS expression expression -> . expression MINUS expression expression -> . expression TIMES expression expression -> . expression DIVIDE expression expression -> . NUMBER expression -> . LPAREN expression RPAREN NUMBER shift and go to state 3 LPAREN shift and go to state 2 state 1 S' -> expression . expression -> expression . PLUS expression expression -> expression . MINUS expression expression -> expression . TIMES expression expression -> expression . DIVIDE expression PLUS shift and go to state 6 MINUS shift and go to state 5 TIMES shift and go to state 4 DIVIDE shift and go to state 7 state 2 expression -> LPAREN . expression RPAREN expression -> . expression PLUS expression expression -> . expression MINUS expression expression -> . expression TIMES expression expression -> . expression DIVIDE expression expression -> . NUMBER expression -> . LPAREN expression RPAREN NUMBER shift and go to state 3 LPAREN shift and go to state 2 state 3 expression -> NUMBER . $ reduce using rule 5 PLUS reduce using rule 5 MINUS reduce using rule 5 TIMES reduce using rule 5 DIVIDE reduce using rule 5 RPAREN reduce using rule 5 state 4 expression -> expression TIMES . expression expression -> . expression PLUS expression expression -> . expression MINUS expression expression -> . expression TIMES expression expression -> . expression DIVIDE expression expression -> . NUMBER expression -> . LPAREN expression RPAREN NUMBER shift and go to state 3 LPAREN shift and go to state 2 state 5 expression -> expression MINUS . expression expression -> . expression PLUS expression expression -> . expression MINUS expression expression -> . expression TIMES expression expression -> . expression DIVIDE expression expression -> . NUMBER expression -> . LPAREN expression RPAREN NUMBER shift and go to state 3 LPAREN shift and go to state 2 state 6 expression -> expression PLUS . expression expression -> . expression PLUS expression expression -> . expression MINUS expression expression -> . expression TIMES expression expression -> . expression DIVIDE expression expression -> . NUMBER expression -> . LPAREN expression RPAREN NUMBER shift and go to state 3 LPAREN shift and go to state 2 state 7 expression -> expression DIVIDE . expression expression -> . expression PLUS expression expression -> . expression MINUS expression expression -> . expression TIMES expression expression -> . expression DIVIDE expression expression -> . NUMBER expression -> . LPAREN expression RPAREN NUMBER shift and go to state 3 LPAREN shift and go to state 2 state 8 expression -> LPAREN expression . RPAREN expression -> expression . PLUS expression expression -> expression . MINUS expression expression -> expression . TIMES expression expression -> expression . DIVIDE expression RPAREN shift and go to state 13 PLUS shift and go to state 6 MINUS shift and go to state 5 TIMES shift and go to state 4 DIVIDE shift and go to state 7 state 9 expression -> expression TIMES expression . expression -> expression . PLUS expression expression -> expression . MINUS expression expression -> expression . TIMES expression expression -> expression . DIVIDE expression $ reduce using rule 3 PLUS reduce using rule 3 MINUS reduce using rule 3 TIMES reduce using rule 3 DIVIDE reduce using rule 3 RPAREN reduce using rule 3 ! PLUS [ shift and go to state 6 ] ! MINUS [ shift and go to state 5 ] ! TIMES [ shift and go to state 4 ] ! DIVIDE [ shift and go to state 7 ] state 10 expression -> expression MINUS expression . expression -> expression . PLUS expression expression -> expression . MINUS expression expression -> expression . TIMES expression expression -> expression . DIVIDE expression $ reduce using rule 2 PLUS reduce using rule 2 MINUS reduce using rule 2 RPAREN reduce using rule 2 TIMES shift and go to state 4 DIVIDE shift and go to state 7 ! TIMES [ reduce using rule 2 ] ! DIVIDE [ reduce using rule 2 ] ! PLUS [ shift and go to state 6 ] ! MINUS [ shift and go to state 5 ] state 11 expression -> expression PLUS expression . expression -> expression . PLUS expression expression -> expression . MINUS expression expression -> expression . TIMES expression expression -> expression . DIVIDE expression $ reduce using rule 1 PLUS reduce using rule 1 MINUS reduce using rule 1 RPAREN reduce using rule 1 TIMES shift and go to state 4 DIVIDE shift and go to state 7 ! TIMES [ reduce using rule 1 ] ! DIVIDE [ reduce using rule 1 ] ! PLUS [ shift and go to state 6 ] ! MINUS [ shift and go to state 5 ] state 12 expression -> expression DIVIDE expression . expression -> expression . PLUS expression expression -> expression . MINUS expression expression -> expression . TIMES expression expression -> expression . DIVIDE expression $ reduce using rule 4 PLUS reduce using rule 4 MINUS reduce using rule 4 TIMES reduce using rule 4 DIVIDE reduce using rule 4 RPAREN reduce using rule 4 ! PLUS [ shift and go to state 6 ] ! MINUS [ shift and go to state 5 ] ! TIMES [ shift and go to state 4 ] ! DIVIDE [ shift and go to state 7 ] state 13 expression -> LPAREN expression RPAREN . $ reduce using rule 6 PLUS reduce using rule 6 MINUS reduce using rule 6 TIMES reduce using rule 6 DIVIDE reduce using rule 6 RPAREN reduce using rule 6 ~~~ 文件中出现的不同状态,代表了有效输入标记的所有可能的组合,这是依据文法规则得到的。当得到输入标记时,分析器将构造一个栈,并找到匹配的规则。每个状态跟踪了当前输入进行到语法规则中的哪个位置,在每个规则中,’.’表示当前分析到规则的哪个位置,而且,对于在当前状态下,输入的每个有效标记导致的动作也被罗列出来。当出现移进/归约或归约/归约冲突时,被忽略的规则前面会添加!,就像这样: ~~~ ! TIMES [ reduce using rule 2 ] ! DIVIDE [ reduce using rule 2 ] ! PLUS [ shift and go to state 6 ] ! MINUS [ shift and go to state 5 ] ~~~ 通过查看这些规则并结合一些实例,通常能够找到大部分冲突的根源。应该强调的是,不是所有的移进归约冲突都是不好的,想要确定解决方法是否正确,唯一的办法就是查看parser.out。 ### 6.8 处理语法错误 如果你创建的分析器用于产品,处理语法错误是很重要的。一般而言,你不希望分析器在遇到错误的时候就抛出异常并终止,相反,你需要它报告错误,尽可能的恢复并继续分析,一次性的将输入中所有的错误报告给用户。这是一些已知语言编译器的标准行为,例如C,C++,Java。在PLY中,在语法分析过程中出现错误,错误会被立即检测到(分析器不会继续读取源文件中错误点后面的标记)。然而,这时,分析器会进入恢复模式,这个模式能够用来尝试继续向下分析。LR分析器的错误恢复是个理论与技巧兼备的问题,yacc.py提供的错误机制与Unix下的yacc类似,所以你可以从诸如O’Reilly出版的《Lex and yacc》的书中找到更多的细节。 当错误发生时,yacc.py按照如下步骤进行: 1. 第一次错误产生时,用户定义的p_error()方法会被调用,出错的标记会作为参数传入;如果错误是因为到达文件结尾造成的,传入的参数将为None。随后,分析器进入到“错误恢复”模式,该模式下不会在产生`p_error()`调用,直到它成功的移进3个标记,然后回归到正常模式。 2. 如果在p_error()中没有指定恢复动作的话,这个导致错误的标记会被替换成一个特殊的error标记。 3. 如果导致错误的标记已经是error的话,原先的栈顶的标记将被移除。 4. 如果整个分析栈被放弃,分析器会进入重置状态,并从他的初始状态开始分析。 5. 如果此时的语法规则接受error标记,error标记会移进栈。 6. 如果当前栈顶是error标记,之后的标记将被忽略,直到有标记能够导致error的归约。 #### 6.8.1 根据error规则恢复和再同步 最佳的处理语法错误的做法是在语法规则中包含error标记。例如,假设你的语言有一个关于print的语句的语法规则: ~~~ def p_statement_print(p): 'statement : PRINT expr SEMI' ... ~~~ 为了处理可能的错误表达式,你可以添加一条额外的语法规则: ~~~ def p_statement_print_error(p): 'statement : PRINT error SEMI' print "Syntax error in print statement. Bad expression" ~~~ 这样(expr错误时),error标记会匹配任意多个分号之前的标记(分号是`SEMI`指代的字符)。一旦找到分号,规则将被匹配,这样error标记就被归约了。 这种类型的恢复有时称为”分析器再同步”。error标记扮演了表示所有错误标记的通配符的角色,而紧随其后的标记扮演了同步标记的角色。 重要的一个说明是,通常error不会作为语法规则的最后一个标记,像这样: ~~~ def p_statement_print_error(p): 'statement : PRINT error' print "Syntax error in print statement. Bad expression" ~~~ 这是因为,第一个导致错误的标记会使得该规则立刻归约,进而使得在后面还有错误标记的情况下,恢复变得困难。 #### 6.8.2 悲观恢复模式 另一个错误恢复方法是采用“悲观模式”:该模式下,开始放弃剩余的标记,直到能够达到一个合适的恢复机会。 悲观恢复模式都是在p_error()方法中做到的。例如,这个方法在开始丢弃标记后,直到找到闭合的’}’,才重置分析器到初始化状态: ~~~ def p_error(p): print "Whoa. You are seriously hosed." # Read ahead looking for a closing '}' while 1: tok = yacc.token() # Get the next token if not tok or tok.type == 'RBRACE': break yacc.restart() ~~~ 下面这个方法简单的抛弃错误的标记,并告知分析器错误被接受了: ~~~ def p_error(p): print "Syntax error at token", p.type # Just discard the token and tell the parser it's okay. yacc.errok() ~~~ 在`p_error()`方法中,有三个可用的方法来控制分析器的行为: * `yacc.errok()` 这个方法将分析器从恢复模式切换回正常模式。这会使得不会产生error标记,并重置内部的error计数器,而且下一个语法错误会再次产生p_error()调用 * `yacc.token()` 这个方法用于得到下一个标记 * `yacc.restart()` 这个方法抛弃当前整个分析栈,并重置分析器为起始状态 注意:这三个方法只能在`p_error()`中使用,不能用在其他任何地方。 p_error()方法也可以返回标记,这样能够控制将哪个标记作为下一个标记返回给分析器。这对于需要同步一些特殊标记的时候有用,比如: ~~~ def p_error(p): # Read ahead looking for a terminating ";" while 1: tok = yacc.token() # Get the next token if not tok or tok.type == 'SEMI': break yacc.errok() # Return SEMI to the parser as the next lookahead token return tok ~~~ #### 6.8.3 从产生式中抛出错误 如果有需要的话,产生式规则可以主动的使分析器进入恢复模式。这是通过抛出`SyntacError`异常做到的: ~~~ def p_production(p): 'production : some production ...' raise SyntaxError ~~~ raise SyntaxError错误的效果就如同当前的标记是错误标记一样。因此,当你这么做的话,最后一个标记将被弹出栈,当前的下一个标记将是error标记,分析器进入恢复模式,试图归约满足error标记的规则。此后的步骤与检测到语法错误的情况是完全一样的,p_error()也会被调用。 手动设置错误有个重要的方面,就是p_error()方法在这种情况下不会调用。如果你希望记录错误,确保在抛出SyntaxError错误的产生式中实现。 注:这个功能是为了模仿yacc中的`YYERROR`宏的行为 #### 6.8.4 错误恢复总结 对于通常的语言,使用error规则和再同步标记可能是最合理的手段。这是因为你可以将语法设计成在一个相对容易恢复和继续分析的点捕获错误。悲观恢复模式只在一些十分特殊的应用中有用,这些应用往往需要丢弃掉大量输入,再寻找合理的同步点。 #### 6.9 行号和位置的跟踪 位置跟踪通常是个设计编译器时的技巧性玩意儿。默认情况下,PLY跟踪所有标记的行号和位置,这些信息可以这样得到: * p.lineno(num)返回第num个符号的行号 * p.lexpos(num)返回第num个符号的词法位置偏移 例如: ~~~ def p_expression(p): 'expression : expression PLUS expression' p.lineno(1) # Line number of the left expression p.lineno(2) # line number of the PLUS operator p.lineno(3) # line number of the right expression ... start,end = p.linespan(3) # Start,end lines of the right expression starti,endi = p.lexspan(3) # Start,end positions of right expression ~~~ 注意:lexspan()方法只会返回的结束位置是最后一个符号的起始位置。 虽然,PLY对所有符号的行号和位置的跟踪很管用,但经常是不必要的。例如,你仅仅是在错误信息中使用行号,你通常可以仅仅使用关键标记的信息,比如: ~~~ def p_bad_func(p): 'funccall : fname LPAREN error RPAREN' # Line number reported from LPAREN token print "Bad function call at line", p.lineno(2) ~~~ 类似的,为了改善性能,你可以有选择性的将行号信息在必要的时候进行传递,这是通过p.set_lineno()实现的,例如: ~~~ def p_fname(p): 'fname : ID' p[0] = p[1] p.set_lineno(0,p.lineno(1)) ~~~ 对于已经完成分析的规则,PLY不会保留行号信息,如果你是在构建抽象语法树而且需要行号,你应该确保行号保留在树上。 ### 6.10 构造抽象语法树 yacc.py没有构造抽像语法树的特殊方法。不过,你可以自己很简单的构造出来。 一个最为简单的构造方法是为每个语法规则创建元组或者字典,并传递它们。有很多中可行的方案,下面是一个例子: ~~~ def p_expression_binop(p): '''expression : expression PLUS expression | expression MINUS expression | expression TIMES expression | expression DIVIDE expression''' p[0] = ('binary-expression',p[2],p[1],p[3]) def p_expression_group(p): 'expression : LPAREN expression RPAREN' p[0] = ('group-expression',p[2]) def p_expression_number(p): 'expression : NUMBER' p[0] = ('number-expression',p[1]) ~~~ 另一种方法可以是为不同的抽象树节点创建一系列的数据结构,并赋值给p[0]: ~~~ class Expr: pass class BinOp(Expr): def __init__(self,left,op,right): self.type = "binop" self.left = left self.right = right self.op = op class Number(Expr): def __init__(self,value): self.type = "number" self.value = value def p_expression_binop(p): '''expression : expression PLUS expression | expression MINUS expression | expression TIMES expression | expression DIVIDE expression''' p[0] = BinOp(p[1],p[2],p[3]) def p_expression_group(p): 'expression : LPAREN expression RPAREN' p[0] = p[2] def p_expression_number(p): 'expression : NUMBER' p[0] = Number(p[1]) ~~~ 这种方式的好处是在处理复杂语义时比较简单:类型检查、代码生成、以及其他针对树节点的功能。 为了简化树的遍历,可以创建一个通用的树节点结构,例如: ~~~ class Node: def __init__(self,type,children=None,leaf=None): self.type = type if children: self.children = children else: self.children = [ ] self.leaf = leaf def p_expression_binop(p): '''expression : expression PLUS expression | expression MINUS expression | expression TIMES expression | expression DIVIDE expression''' p[0] = Node("binop", [p[1],p[3]], p[2]) ~~~ ### 6.11 嵌入式动作 yacc使用的分析技术只允许在规则规约后执行动作。假设有如下规则: ~~~ def p_foo(p): "foo : A B C D" print "Parsed a foo", p[1],p[2],p[3],p[4] ~~~ 方法只会在符号A,B,C和D都完成后才能执行。可是有的时候,在中间阶段执行一小段代码是有用的。假如,你想在A完成后立即执行一些动作,像下面这样用空规则: ~~~ def p_foo(p): "foo : A seen_A B C D" print "Parsed a foo", p[1],p[3],p[4],p[5] print "seen_A returned", p[2] def p_seen_A(p): "seen_A :" print "Saw an A = ", p[-1] # Access grammar symbol to left p[0] = some_value # Assign value to seen_A ~~~ 在这个例子中,空规则seen_A将在A移进分析栈后立即执行。p[-1]指代的是在分析栈上紧跟在seen_A左侧的符号。在这个例子中,是A符号。像其他普通的规则一样,在嵌入式行为中也可以通过为p[0]赋值来返回某些值。 使用嵌入式动作可能会导致移进归约冲突,比如,下面的语法是没有冲突的: ~~~ def p_foo(p): """foo : abcd | abcx""" def p_abcd(p): "abcd : A B C D" def p_abcx(p): "abcx : A B C X" ~~~ 可是,如果像这样插入一个嵌入式动作: ~~~ def p_foo(p): """foo : abcd | abcx""" def p_abcd(p): "abcd : A B C D" def p_abcx(p): "abcx : A B seen_AB C X" def p_seen_AB(p): "seen_AB :" ~~~ 会产生移进归约冲,只是由于对于两个规则abcd和abcx中的C,分析器既可以根据abcd规则移进,也可以根据abcx规则先将空的seen_AB归约。 嵌入动作的一般用于分析以外的控制,比如为本地变量定义作用于。对于C语言: ~~~ def p_statements_block(p): "statements: LBRACE new_scope statements RBRACE""" # Action code ... pop_scope() # Return to previous scope def p_new_scope(p): "new_scope :" # Create a new scope for local variables s = new_scope() push_scope(s) ... ~~~ 在这个例子中,new_scope作为嵌入式行为,在左大括号{之后立即执行。可以是调正内部符号表或者其他方面。statements_block一完成,代码可能会撤销在嵌入动作时的操作(比如,pop_scope()) ### 6.12 Yacc的其他 * 默认的分析方法是LALR,使用SLR请像这样运行 yacc():yacc.yacc(method=”SLR”)注意:LRLR生成的分析表大约要比SLR的大两倍。解析的性能没有本质的区别,因为代码是一样的。由于LALR能力稍强,所以更多的用于复杂的语法。 * 默认情况下,yacc.py依赖lex.py产生的标记。不过,可以用一个等价的词法标记生成器代替: yacc.parse(lexer=x) 这个例子中,x必须是一个Lexer对象,至少拥有x.token()方法用来获取标记。如果将输入字串提供给yacc.parse(),lexer还必须具有x.input()方法。 * 默认情况下,yacc在调试模式下生成分析表(会生成parser.out文件和其他东西),使用yacc.yacc(debug=0)禁用调试模式。 * 改变parsetab.py的文件名:yacc.yacc(tabmodule=”foo”) * 改变parsetab.py的生成目录:yacc.yacc(tabmodule=”foo”,outputdir=”somedirectory”) * 不生成分析表:yacc.yacc(write_tables=0)。注意:如果禁用分析表生成,yacc()将在每次运行的时候重新构建分析表(这里耗费的时候取决于语法文件的规模) * 想在分析过程中输出丰富的调试信息,使用:yacc.parse(debug=1) * yacc.yacc()方法会返回分析器对象,如果你想在一个程序中支持多个分析器: ~~~ p = yacc.yacc() ... p.parse() ~~~ 注意:yacc.parse()方法只绑定到最新创建的分析器对象上。 * 由于生成生成LALR分析表相对开销较大,先前生成的分析表会被缓存和重用。判断是否重新生成的依据是对所有的语法规则和优先级规则进行MD5校验,只有不匹配时才会重新生成。生成分析表是合理有效的办法,即使是面对上百个规则和状态的语法。对于复杂的编程语言,像C语言,在一些慢的机器上生成分析表可能要花费30-60秒,请耐心。 * 由于LR分析过程是基于分析表的,分析器的性能很大程度上取决于语法的规模。最大的瓶颈可能是词法分析器和语法规则的复杂度。 ## 7 多个语法和词法分析器 在高级的分析器程序中,你可能同时需要多个语法和词法分析器。 依照规则行事不会有问题。不过,你需要小心确定所有东西都正确的绑定(hooked up)了。首先,保证将lex()和yacc()返回的对象保存起来: ~~~ lexer = lex.lex() # Return lexer object parser = yacc.yacc() # Return parser object ~~~ 接着,在解析时,确保给parse()方法一个正确的lexer引用: ~~~ parser.parse(text,lexer=lexer) ~~~ 如果遗漏这一步,分析器会使用最新创建的lexer对象,这可能不是你希望的。 词法器和语法器的方法中也可以访问这些对象。在词法器中,标记的lexer属性指代的是当前触发规则的词法器对象: ~~~ def t_NUMBER(t): r'\d+' ... print t.lexer # Show lexer object ~~~ 在语法器中,lexer和parser属性指代的是对应的词法器对象和语法器对象 ~~~ def p_expr_plus(p): 'expr : expr PLUS expr' ... print p.parser # Show parser object print p.lexer # Show lexer object ~~~ 如果有必要,lexer对象和parser对象都可以附加其他属性。例如,你想要有不同的解析器状态,可以为为parser对象附加更多的属性,并在后面用到它们。 ## 8 使用Python的优化模式 由于PLY从文档字串中获取信息,语法解析和词法分析信息必须通过正常模式下的Python解释器得到(不带有-O或者-OO选项)。不过,如果你像这样指定optimize模式: ~~~ lex.lex(optimize=1) yacc.yacc(optimize=1) ~~~ PLY可以在下次执行,在Python的优化模式下执行。但你必须确保第一次执行是在Python的正常模式下进行,一旦词法分析表和语法分析表生成一次后,在Python优化模式下执行,PLY会使用生成好的分析表而不再需要文档字串。 注意:在优化模式下执行PLY会禁用很多错误检查机制。你应该只在程序稳定后,不再需要调试的情况下这样做。使用优化模式的目的应该是大幅减少你的编译器的启动时间(万事俱备只欠东风时) ## 9 高级调试 调试一个编译器不是件容易的事情。PLY提供了一些高级的调试能力,这是通过Python的logging模块实现的,下面两节介绍这一主题: ### 9.1 调试lex()和yacc()命令 lex()和yacc()命令都有调试模式,可以通过debug标识实现: ~~~ lex.lex(debug=True) yacc.yacc(debug=True) ~~~ 正常情况下,调试不仅输出标准错误,对于yacc(),还会给出parser.out文件。这些输出可以通过提供logging对象来精细的控制。下面这个例子增加了对调试信息来源的输出: ~~~ # Set up a logging object import logging logging.basicConfig( level = logging.DEBUG, filename = "parselog.txt", filemode = "w", format = "%(filename)10s:%(lineno)4d:%(message)s" ) log = logging.getLogger() lex.lex(debug=True,debuglog=log) yacc.yacc(debug=True,debuglog=log) ~~~ 如果你提供一个自定义的logger,大量的调试信息可以通过分级来控制。典型的是将调试信息分为DEBUG,INFO,或者WARNING三个级别。 PLY的错误和警告信息通过日志接口提供,可以从errorlog参数中传入日志对象 ~~~ lex.lex(errorlog=log) yacc.yacc(errorlog=log) ~~~ 如果想完全过滤掉警告信息,你除了可以使用带级别过滤功能的日志对象,也可以使用lex和yacc模块都内建的Nulllogger对象。例如: ~~~ yacc.yacc(errorlog=yacc.NullLogger()) ~~~ ### 9.2 运行时调试 为分析器指定debug选项,可以激活语法分析器的运行时调试功能。这个选项可以是整数(表示对调试功能是开还是关),也可以是logger对象。例如: ~~~ log = logging.getLogger() parser.parse(input,debug=log) ~~~ 如果传入日志对象的话,你可以使用其级别过滤功能来控制内容的输出。INFO级别用来产生归约信息;DEBUG级别会显示分析栈的信息、移进的标记和其他详细信息。ERROR级别显示分析过程中的错误相关信息。 对于每个复杂的问题,你应该用日志对象,以便输出重定向到文件中,进而方便在执行结束后检查。 ## 10 如何继续 PLY分发包中的example目录包含几个简单的示例。对于理论性的东西以及LR分析发的实现细节,应当从编译器相关的书籍中学习。