PLY (Python Lex-Yacc)
html
本文档提供了使用PLY进行词法分析和解析的概述,考虑到解析的内在复杂性,我强烈建议您在使用PLY进行大型开发项目以前阅读(或至少略读)整个文档。node
PLY是流行的编译器构造工具lex和yacc的纯python实现。PLY的主要目标是至关忠实于传统lex/yacc工具的工做方式。这包括支持LALR(1)解析,以及提供普遍的输入验证、错误报告和诊断。所以,若是您在另外一种编程语言中使用过yacc,那么使用PLY应该相对简单。python
PLY的早期版本是为了支持David 2001年在芝加哥大学(University of Chicago)教授的编译器入门课程而开发的。因为PLY最初是做为教学工具开发的,因此您会发现它对标记和语法规则规范至关挑剔。在某种程度上,这种附加的形式是为了捕捉新手用户所犯的常见编程错误。然而,高级用户在为真正的编程语言构建复杂语法时也会发现这些特性很是有用。还应该注意,PLY没有提供太多额外功能(例如,自动构造抽象语法树、遍历树等)。我也不认为它是一个解析框架。相反,您将发现一个彻底用Python编写的、功能齐全的基本lex/yacc实现。c++
本文的其他部分须要假设您对解析理论、语法制导翻译以及其余编程语言中编译器构造工具(如lex和yacc)的使用有必定的了解。若是您不熟悉这些主题,您可能想要查阅介绍性的文本,例如Aho、Sethi和Ullman编写的“编译器:原则、技术和工具”。O’Reilly的John Levine的《Lex and Yacc》可能也很方便。事实上,O’Reilly的书能够做为PLY的参考,由于这两个概念其实是相同的。git
PLY由两个单独的模块组成:lex.py 和 yacc. p y。均可以在名为ply的Python包中找到。web
lex.py模块用于将输入的文本经过正则表达式转化成一系列特定的token。正则表达式
yacc.py用于识别以上下文无关语法形式指定的语言语法。算法
这两种工具应该一块儿工做。具体来讲,lex.py以token()函数的形式提供了一个外部接口,该函数返回输入流上的下一个有效token。yacc.py用于反复调用函数来检索标记并调用语法规则。yacc.py的输出一般是抽象语法树(AST)。然而,这彻底取决于用户。若是须要,还可使用yacc.py实现简单的单遍编译器。express
与Unix对应程序同样,yacc.py提供了您指望的大部分特性,包括普遍的错误检查、语法验证、对空结果的支持、错误标记以及经过优先规则解决歧义。事实上,几乎全部在传统yacc中能够实现的功能都应该获得充分的支持。编程
yacc.py和Unix 中yacc的主要区别在于,yacc.py不涉及单独的代码生成过程。相反,PLY依赖于反射(内省)来构建它的词法分析器和解析器。与传统的lex/yacc不一样,传统的lex/yacc须要将一个特殊的输入文件转换为一个单独的源文件,而PLY的规范是有效的Python程序。
这意味着没有额外的源文件,也没有特殊的编译器构造步骤(例如,运行yacc为编译器生成Python代码)。因为生成解析表的成本相对较高,PLY缓存结果并将其保存到文件中。若是在输入源中没有检测到任何更改,则从缓存中读取表。不然,它们将被从新生成。
lex.py用于将输入的代码转化为token序列,假设你将输入的程序语言为以下的语句:
x = 3 + 42 * (s - t)
一个分词器将会把这语句分割成单个的token:
'x','=', '3', '+', '42', '*', '(', 's', '-', 't', ')'
Token一般须要给一个名字来标注它们是哪一个类型的,例如:
'ID','EQUALS','NUMBER','PLUS','NUMBER','TIMES', 'LPAREN','ID','MINUS','ID','RPAREN'
更具体的来说,输入被分红包含类型和相应值的序列,例如:
('ID','x'), ('EQUALS','='), ('NUMBER','3'), ('PLUS','+'), ('NUMBER','42), ('TIMES','*'), ('LPAREN','('), ('ID','s'), ('MINUS','-'), ('ID','t'), ('RPAREN',')'
一般状况下经过一系列的正则表达式来肯定token,下一节未来具体介绍它在lex.py中如何实现。
下面的例子将介绍lex.py将如何实现一个简单的分词器。
# ------------------------------------------------------------ # calclex.py # # tokenizer for a simple expression evaluator for # numbers and +,-,*,/ # ------------------------------------------------------------ import ply.lex as lex # List of token names. This is always required tokens = ( 'NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN', ) # Regular expression rules for simple tokens t_PLUS = r'\+' t_MINUS = r'-' t_TIMES = r'\*' t_DIVIDE = r'/' t_LPAREN = r'\(' t_RPAREN = r'\)' # A regular expression rule with some action code def t_NUMBER(t): r'\d+' t.value = int(t.value) return t # Define a rule so we can track line numbers def t_newline(t): r'\n+' t.lexer.lineno += len(t.value) # A string containing ignored characters (spaces and tabs) t_ignore = ' \t' # Error handling rule def t_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1) # Build the lexer lexer = lex.lex()
要想使用这个词法分析程序,你首先须要使用input()方法输入一些文本,而后重复调用token()方法生成token序列,以下代码是展现了它是如何运做的。
# Test it out data = ''' 3 + 4 * 10 + -20 *2 ''' # Give the lexer some input lexer.input(data) # Tokenize while True: tok = lexer.token() if not tok: break # No more input print(tok)
执行的结果为:
LexToken(NUMBER,3,2,1) LexToken(PLUS,'+',2,3) LexToken(NUMBER,4,2,5) LexToken(TIMES,'*',2,7) LexToken(NUMBER,10,2,10) LexToken(PLUS,'+',3,14) LexToken(MINUS,'-',3,16) LexToken(NUMBER,20,3,18) LexToken(TIMES,'*',3,20) LexToken(NUMBER,2,3,21)
lexer.token()方法会返回一个LexToken的实例,该对象中的属性包括:tok.type
, tok.value
, tok.lineno
和tok.lexpos
。
其中type和value指的是token中的类型和相应的值,tok.lineno
和tok.lexpos
表示的这个token的位置,tok.lexpos
是相对应的这段输入文本起始地址的相对地址。
例如“3”这个数字表示了位于data中第2行,是data第一个元素,“+”(第三行)表示了位于data中第3行,是data第14个元素。注意一个空格也算占一个位置。
全部的词法分析程序必须定义tokens来代表全部可能的token名称,只有这些名称将会被词法分析程序来处理。定义这个序列每每是必要的,也被用于执行各类验证检查。并且在yacc.py模块中也用于肯定终结符。
例如,下列代码肯定了一系列的token名称:
tokens = ( 'NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN', )
每一个token都是经过编写与Python的re模块兼容的正则表达式规则来指定的。每一个规则都是经过使用一个特殊前缀t_声明来定义的,以代表它定义了一个token。
对于简单的token,正则表达式能够指定为这样的字符串(注意:使用Python原始字符串,由于它们是编写正则表达式字符串最方便的方法):
t_PLUS = r'\+'
在这种状况下,t_后面的名称必须与tokens中提供的名称彻底匹配。若是须要执行某种操做,能够将令牌规则指定为函数。例如,该规则匹配数字并将字符串转换为Python整数。
def t_NUMBER(t): r'\d+' t.value = int(t.value) return t
当使用函数时,正则表达式规则在函数文档字符串中指定。该函数老是接受单个参数,即LexToken的实例。每一个LexToken中含有四种属性:
t.type
:类型t.value
:值t.lineno
:标明第几行t.lexpos
:相对于文本起始位置的相对位置一般状况下,t.type的名称与t_后跟的名称一致,action函数能够根据须要修改LexToken对象的内容。可是,当它完成时,应该返回生成的token。若是action函数没有返回值,则只丢弃令牌并读取下一个token。
在内部,lex.py使用re模块进行模式匹配。模式是使用re.VERBOSE标志编译的,该标志可用于提升可读性。可是,请注意,未转义的空格将被忽略,而且在此模式下容许注释。若是您的模式包含空格,请确保使用\s。若是须要匹配#字符,请使用[#]。
构建主正则表达式时,按以下顺序添加规则:
1)函数定义的全部token都按照它们在lexer文件中出现的相同顺序添加。
2)按照正则表达式长度递减的顺序对字符串定义的标记进行排序(首先添加更长的表达式)。
若是没有这种排序,就很难正确匹配某些类型的token。例如,若是您想为“=”和“==” 使用单独的标记,您须要确保首先选中了“==”。经过对正则表达式按长度递减排序,解决了定义为字符串的规则的排序问题。对于函数,能够显式地控制顺序,由于首先检查出现的规则。
要处理保留字,您应该编写一个规则来匹配标识符,并在函数中执行一个特殊的名称查找,以下所示:
reserved = { 'if' : 'IF', 'then' : 'THEN', 'else' : 'ELSE', 'while' : 'WHILE', ... } tokens = ['LPAREN','RPAREN',...,'ID'] + list(reserved.values()) def t_ID(t): r'[a-zA-Z_][a-zA-Z_0-9]*' t.type = reserved.get(t.value,'ID')# Check for reserved words return t
这种方法极大地减小了正则表达式规则的数量,并可能使事情更快一些。
注意:您应该避免为保留字编写单独的规则。例如,若是你这样写规则:
t_FOR = r'for' t_PRINT = r'print'
对于包含这些单词做为前缀的标识符,如“forget”或“printed”,将触发这些规则。这可能不是你想要的。
当lex返回token时,它们有一个值存储在value属性中。一般,值是匹配的文本。可是,该值能够分配给任何Python对象。例如,在对标识符进行词法分析时,可能但愿同时返回标识符名称和来自某种符号表的信息。要作到这一点,你能够这样写一条规则:
def t_ID(t): ... # Look up symbol table information and return a tuple t.value = (t.value, symbol_lookup(t.value)) ... return t
须要注意的是,不建议使用其余属性名存储数据。yacc.py模块只公开value属性的内容。所以,访问其余属性多是没必要要的尴尬。若是须要在token上存储多个值,请将元组、字典或实例赋值。
若是是注释,则须要跳过对应标志,例如:
def t_COMMENT(t): r'\#.*' pass # No return value. Token discarded
或者,您能够在token声明中包含前缀“ignore_”,以强制忽略token。例如:
t_ignore_COMMENT = r'\#.*'
请注意,若是忽略了许多不一样类型的文本,您可能仍然但愿使用函数,由于这些函数提供了对正则表达式匹配顺序的更精确控制(即,函数按指定的顺序匹配,而字符串按正则表达式长度排序)。
默认状况下,lex.py并不知道行号信息,由于它并不清楚是什么构成了一行(例如一个换行符)。若是要更新这个信息,则须要写一个规则,如:
# Define a rule so we can track line numbers def t_newline(t): r'\n+' t.lexer.lineno += len(t.value)
在这个规则下,lineno
属性就会更新了,当这个行号更新之后,这个token就直接被忽略了,而不用返回值。
lex.py没有使用任何自动的列追踪,然而它确实在lexpos
中记录了token相关的位置信息,使用这种方法,一般能够将计算列信息做为一个单独的步骤。例如,只需向后计数,直到到达换行为止。
# Compute column. # input is the input text string # token is a token instance def find_column(input, token): line_start = input.rfind('\n', 0, token.lexpos) + 1 return (token.lexpos - line_start) + 1
因为列信息一般只在错误处理上下文中有用,所以能够在须要时计算列位置,而不是对每一个token进行计算。
对于输入流中应该彻底忽略的字符,lex.py保留了特殊的t_ignore规则。一般这是用来跳过空白和其余没必要要的字符。尽管能够以相似于t_newline()的方式为空白定义正则表达式规则,可是t_ignore的使用提供了更好的词法分析性能,由于它是做为一种特殊状况处理的,而且以比常规正则表达式规则更有效的方式进行检查。
当t_ignore中给出的字符是其余正则表达式模式的一部分时,这些字符不会被忽略。例如,若是您有一个捕获引用文本的规则,那么该模式能够包含被忽略的字符(将以正常方式捕获这些字符)。t_ignore的主要目的是忽略要解析的标记之间的空格和其余填充。
文字字符能够经过在词法分析模块中定义变量文字来指定。例如:
literals = [ '+','-','*','/' ]
或者:
literals = "+-*/"
文字字符只是lexer遇到时返回“原样”的单个字符。
在全部定义的正则表达式规则以后检查文本。所以,若是规则以其中一个文字字符开始,它老是优先。
当返回文字标记时,它的类型和值属性都设置为字符自己。例如,“+”。
当字面值匹配时,能够编写执行附加操做的token函数。可是,您须要适当地设置token类型。例如:
literals = [ '{', '}' ] def t_lbrace(t): r'\{' t.type = '{' # Set token type to the expected literal return t def t_rbrace(t): r'\}' t.type = '}' # Set token type to the expected literal return t
t_error函数的做用是:处理检测到非法字符时发生的词法分析错误。
# Error handling rule def t_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1)
在本例中,咱们只需打印出违规字符并经过调用t.lexer.skip(1)跳过一个字符。
t_eof()函数的做用是:处理输入中的文件结束(EOF)条件。做为输入,它接收一个token类型“eof”,并适当设置lineno和lexpos属性。这个函数的主要用途是为lexer提供更多的输入,以便它可以继续解析。下面是一个例子:
# EOF handling rule def t_eof(t): # Get more input (Example) more = raw_input('... ') if more: self.lexer.input(more) return self.lexer.token() return None
EOF函数应该返回下一个可用的token(经过调用self.lexer.token()或None来指示没有更多的数据。注意,使用self.lexer.input()方法设置更多的输入并不会重置lexer状态或用于位置跟踪的lineno属性。lexpos属性被重置,因此若是在错误报告中使用它,请注意这一点。
构建一个词法分析器的方法为:
lexer = lex.lex()
该函数使用Python反射(或内省)从调用上下文中读取正则表达式规则并构建lexer。一旦构建了lexer,就可使用两种方法来控制lexer。
lexer.input(data)
. 根据输入的data从新设置词法分析器lexer.token()
. 返回下一个token。在某些应用程序中,您可能但愿将构建token定义为一系列更复杂的正则表达式规则。例如:
digit = r'([0-9])' nondigit = r'([_A-Za-z])' identifier = r'(' + nondigit + r'(' + digit + r'|' + nondigit + r')*)' def t_ID(t): # want docstring to be identifier above. ????? ...
在本例中,咱们但愿ID的正则表达式规则是上面的变量之一。可是,没法使用普通的文档字符串直接指定它。要解决这个问题,可使用@TOKEN装饰器。例如:
from ply.lex import TOKEN @TOKEN(identifier) def t_ID(t): ...
这将为t_ID()附加标识符,容许lex.py正常工做。
为了提升性能,最好使用Python的优化模式(例如,使用-O选项运行Python)。可是,这样作会致使Python忽略文档字符串。这给lex.py带来了特殊的问题。要处理这种状况,可使用以下优化选项建立lexer:
lexer = lex.lex(optimize=1)
接下来,以正常的操做模式运行Python。当您这样作时,lex.py将在包含lexer规范的模块所在的目录中编写一个名为lextab.py的文件。该文件包含在词法分析期间使用的全部正则表达式规则和表。在随后的执行中,只须要导入lextab.py来构建lexer。这种方法大大提升了lexer的启动时间,而且能够在Python的优化模式下工做。
要更改lexer生成的模块的名称,请使用lextab关键字参数。例如:
lexer = lex.lex(optimize=1,lextab="footab")
在优化模式下运行时,必须注意lex禁用了大多数错误检查。所以,只有在您确信全部操做都是正确的,而且准备好开始发布生产代码时,才真正推荐这样作。
为了调试,您能够在调试模式下运行lex(),以下所示:
lexer = lex.lex(debug=1)
这将生成各类调试信息,包括全部添加的规则、lexer使用的主正则表达式和词法分析期间生成的标记。
此外,lex.py附带了一个简单的main函数,它能够标记从标准输入读取的输入,也能够标记从命令行指定的文件读取的输入。要使用它,只需把它放进你的词典:
if __name__ == '__main__': lex.runmain()
有关调试的更高级细节,请参阅末尾的“调试”一节。
如示例所示,lexer都是在一个Python模块中指定的。若是但愿将token规则放在与调用lex()的模块不一样的模块中,请使用module关键字参数。
例如,您可能有一个专门的模块,它只包含token规则:
# This module just contains the lexing rules # List of token names. This is always required tokens = ( 'NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN', ) # Regular expression rules for simple tokens t_PLUS = r'\+' t_MINUS = r'-' t_TIMES = r'\*' t_DIVIDE = r'/' t_LPAREN = r'\(' t_RPAREN = r'\)' # A regular expression rule with some action code def t_NUMBER(t): r'\d+' t.value = int(t.value) return t # Define a rule so we can track line numbers def t_newline(t): r'\n+' t.lexer.lineno += len(t.value) # A string containing ignored characters (spaces and tabs) t_ignore = ' \t' # Error handling rule def t_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1)
使用这个token规则的代码以下:
>>> import tokrules >>> lexer = lex.lex(module=tokrules) >>> lexer.input("3 + 4") >>> lexer.token() LexToken(NUMBER,3,1,1,0) >>> lexer.token() LexToken(PLUS,'+',1,2) >>> lexer.token() LexToken(NUMBER,4,1,4) >>> lexer.token() None
模块选项还能够用于从类的实例定义lexer。例如:
import ply.lex as lex class MyLexer(object): # List of token names. This is always required tokens = ( 'NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN', ) # Regular expression rules for simple tokens t_PLUS = r'\+' t_MINUS = r'-' t_TIMES = r'\*' t_DIVIDE = r'/' t_LPAREN = r'\(' t_RPAREN = r'\)' # A regular expression rule with some action code # Note addition of self parameter since we're in a class def t_NUMBER(self,t): r'\d+' t.value = int(t.value) return t # Define a rule so we can track line numbers def t_newline(self,t): r'\n+' t.lexer.lineno += len(t.value) # A string containing ignored characters (spaces and tabs) t_ignore = ' \t' # Error handling rule def t_error(self,t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1) # Build the lexer def build(self,**kwargs): self.lexer = lex.lex(module=self, **kwargs) # Test it output def test(self,data): self.lexer.input(data) while True: tok = self.lexer.token() if not tok: break print(tok) # Build the lexer and try it out m = MyLexer() m.build() # Build the lexer m.test("3 + 4") # Test it
在从类构建lexer时,应该从类的实例而不是类对象自己构建lexer。这是由于PLY只有在lexer操做由绑定方法定义时才能正常工做。
当使用lex()的模块选项时,PLY使用dir()函数从底层对象收集符号。不能直接访问做为模块值提供的对象的_dict__属性。
最后,若是您但愿保持良好的封装,但不但愿使用完整的类定义,可使用闭包定义lexer。例如:
import ply.lex as lex # List of token names. This is always required tokens = ( 'NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN', ) def MyLexer(): # Regular expression rules for simple tokens t_PLUS = r'\+' t_MINUS = r'-' t_TIMES = r'\*' t_DIVIDE = r'/' t_LPAREN = r'\(' t_RPAREN = r'\)' # A regular expression rule with some action code def t_NUMBER(t): r'\d+' t.value = int(t.value) return t # Define a rule so we can track line numbers def t_newline(t): r'\n+' t.lexer.lineno += len(t.value) # A string containing ignored characters (spaces and tabs) t_ignore = ' \t' # Error handling rule def t_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1) # Build the lexer from my environment and return it return lex.lex()
重要提示:若是您使用类或闭包定义lexer,请注意PLY仍然要求您仅为每一个模块(源文件)定义一个lexer。若是不遵循这条规则,那么层中有大量的验证/错误检查部分可能会错误地报告错误消息。
在lexer中,您可能但愿维护各类状态信息。这可能包括模式设置、符号表和其余细节。做为一个例子,假设您想跟踪遇到了多少个NUMBER 。
一种方法是在建立lexer的模块中保留一组全局变量。例如:
num_count = 0 def t_NUMBER(t): r'\d+' global num_count num_count += 1 t.value = int(t.value) return t
若是您不喜欢使用全局变量,那么能够在lex()建立的Lexer对象中存储信息。为此,您可使用传递给各类规则的令牌的lexer属性。例如:
def t_NUMBER(t): r'\d+' t.lexer.num_count += 1 # Note use of lexer attribute t.value = int(t.value) return t lexer = lex.lex() lexer.num_count = 0 # Set the initial count
后一种方法的优势是简单,能够在同一个应用程序中存在多个给定lexer实例的应用程序中正确工做。然而,对于OO纯粹主义者来讲,这也多是对封装的严重违反。为了让您放心,lexer的全部内部属性(lineno除外)都有以lex为前缀的名称(例如,lexdata、lexpos等)。所以,在lexer中存储没有以该前缀开头的名称或与预约义方法(例如input()、token()等)冲突的名称的属性是彻底安全的。
若是不喜欢对lexer对象赋值,能够将lexer定义为一个类,以下面的部分所示:
class MyLexer: ... def t_NUMBER(self,t): r'\d+' self.num_count += 1 t.value = int(t.value) return t def build(self, **kwargs): self.lexer = lex.lex(object=self,**kwargs) def __init__(self): self.num_count = 0
若是您的应用程序要建立同一个lexer的多个实例,而且须要管理大量状态,那么类方法多是最容易管理的。
状态也能够经过闭包来管理。例如,在python3中:
def MyLexer(): num_count = 0 ... def t_NUMBER(t): r'\d+' nonlocal num_count num_count += 1 t.value = int(t.value) return t ...
若是须要,能够经过调用它的clone()方法复制lexer对象。例如:
lexer = lex.lex() ... newlexer = lexer.clone()
克隆lexer时,该副本与原始lexer彻底相同,包括任何输入文本和内部状态。可是,克隆容许提供一组不一样的输入文本,这些文本能够单独处理。在编写涉及递归或可重入处理的解析器/编译器时,这可能颇有用。例如,若是出于某种缘由须要提早扫描输入,能够建立一个克隆并使用它来提早查看。或者,若是您正在实现某种预处理器,可使用克隆的lexer来处理不一样的输入文件。
建立克隆与调用lex.lex()不一样,由于PLY不会从新生成任何内部表或正则表达式。
在克隆还使用类或闭包维护自身内部状态的lexer时,须要特别注意。也就是说,您须要知道新建立的lexer将与原始lexer共享全部这些状态。例如,若是您将lexer定义为一个类并执行如下操做:
m = MyLexer() a = lex.lex(object=m) # Create a lexer b = a.clone() # Clone the lexer
而后a和b都将绑定到同一个对象m上,对m的任何更改都将反映在两个lexer中。须要强调的是,clone()只意味着建立一个新的lexer,它重用另外一个lexer的正则表达式和环境。若是须要建立一个全新的lexer副本,那么再次调用lex()。
Lexer对象Lexer具备许多内部属性,这些属性在某些状况下可能有用。
lexer.lexpos
此属性是一个整数,包含输入文本中的当前位置。若是修改该值,它将更改对token()的下一个调用的结果。在token规则函数中,它指向匹配文本以后的第一个字符。若是在规则中修改了该值,则将在新位置匹配下一个返回的token。
lexer.lineno
存储在lexer中的行号属性的当前值。PLY只指定属性存在——它从不设置、更新或执行任何处理。若是您想跟踪行号,您须要本身添加代码(请参阅行号和位置信息一节)。
lexer.lexdata
存储在lexer中的当前输入文本。这是经过input()方法传递的字符串。除非你真的知道你在作什么,不然修改它可能不是一个好主意。
lexer.lexmatch
这是Python re.match()函数(PLY在内部使用)为当前令牌返回的原始匹配对象。若是您编写了包含命名组的正则表达式,则可使用它检索这些值。注意:此属性仅在由函数定义和处理token时更新。
在高级解析应用程序中,具备不一样的词法分析状态可能颇有用。例如,您可能但愿某个token或语法结构的出现触发另外一种类型的词法分析。PLY支持一个特性,该特性容许将底层lexer放入一系列不一样的状态。每一个状态均可以有本身的token、词法规则等等。该实现主要基于GNU flex的“启动条件”特性。详细信息能够在http://flex.sourceforge.net/manual/startconditions .html中找到。
要定义一个新的lexing状态,必须首先声明它。这是经过在lex文件中包含一个“states”声明来实现的。例如:
states = ( ('foo','exclusive'), ('bar','inclusive'), )
这个声明声明了两个状态,‘foo’和’bar’。状态可分为两类;“独占exclusive”和“包含inclusive”。独占状态彻底覆盖lexer的默认行为。也就是说,lex只返回token并应用为该状态定义的规则。包含状态将附加状态和规则添加到默认规则集。所以,除了为包含状态定义的包含以外,lex还将返回默认定义的两个包含。
一旦声明了状态,就经过在token/规则声明中包含状态名来声明token和规则。例如:
t_foo_NUMBER = r'\d+' # Token 'NUMBER' in state 'foo' t_bar_ID = r'[a-zA-Z_][a-zA-Z0-9_]*' # Token 'ID' in state 'bar' def t_foo_newline(t): r'\n' t.lexer.lineno += 1
经过在声明中包含多个状态名,能够在多个状态中声明token。例如:
t_foo_bar_NUMBER = r'\d+' # Defines token 'NUMBER' in both state 'foo' and 'bar'
另外一种方法是,能够在全部状态中使用名称中的“ANY”声明令牌:
t_ANY_NUMBER = r'\d+' # Defines a token 'NUMBER' in all states
若是没有提供状态名(一般是这种状况),则令牌与一个特殊的状态“INITIAL”关联。例如,这两个声明是相同的:
t_foo_ignore = " \t\n" # Ignored characters for state 'foo' def t_bar_error(t): # Special error handler for state 'bar' pass
默认状况下,lexing在“初始”状态下运行。此状态包括全部一般定义的令牌。对于不使用不一样状态的用户,这个事实是彻底透明的。若是在进行词法分析或解析时,但愿更改词法分析状态,请使用begin()方法。例如:
def t_begin_foo(t): r'start_foo' t.lexer.begin('foo') # Starts 'foo' state
要脱离状态,可使用begin()切换回初始状态。例如:
def t_foo_end(t): r'end_foo' t.lexer.begin('INITIAL') # Back to the initial state
状态管理也能够经过堆栈来完成。例如:
def t_begin_foo(t): r'start_foo' t.lexer.push_state('foo') # Starts 'foo' state def t_foo_end(t): r'end_foo' t.lexer.pop_state() # Back to the previous state
堆栈的使用在如下状况下很是有用:有许多方法能够进入新的lexing状态,而您只是想在之后返回到之前的状态。
举个例子可能更清晰。假设您正在编写一个解析器,而且但愿获取用大括号括起来的任意C代码段。也就是说,每当遇到开始大括号“{”时,都但愿读取所包含的全部代码,直到结束大括号“}”,并将其做为字符串返回。使用普通正则表达式规则来实现这一点几乎是不可能的。这是由于大括号能够嵌套,能够包含在注释和字符串中。所以,仅仅匹配第一个匹配的“}”字符是不够的。下面是使用lexer状态的方法:
# Declare the state states = ( ('ccode','exclusive'), ) # Match the first {. Enter ccode state. def t_ccode(t): r'\{' t.lexer.code_start = t.lexer.lexpos # Record the starting position t.lexer.level = 1 # Initial brace level t.lexer.begin('ccode') # Enter 'ccode' state # Rules for the ccode state def t_ccode_lbrace(t): r'\{' t.lexer.level +=1 def t_ccode_rbrace(t): r'\}' t.lexer.level -=1 # If closing brace, return the code fragment if t.lexer.level == 0: t.value = t.lexer.lexdata[t.lexer.code_start:t.lexer.lexpos+1] t.type = "CCODE" t.lexer.lineno += t.value.count('\n') t.lexer.begin('INITIAL') return t # C or C++ comment (ignore) def t_ccode_comment(t): r'(/\*(.|\n)*?\*/)|(//.*)' pass # C string def t_ccode_string(t): r'\"([^\\\n]|(\\.))*?\"' # C character literal def t_ccode_char(t): r'\'([^\\\n]|(\\.))*?\'' # Any sequence of non-whitespace characters (not braces, strings) def t_ccode_nonspace(t): r'[^\s\{\}\'\"]+' # Ignored characters (whitespace) t_ccode_ignore = " \t\n" # For bad characters, we just skip over it def t_ccode_error(t): t.lexer.skip(1)
在本例中,第一个“{”的出现致使lexer记录起始位置并输入一个新的状态“ccode”。而后,一组规则匹配随后输入的各个部分(注释、字符串等)。全部这些规则都只是丢弃令牌(经过不返回值)。可是,若是遇到右大括号,规则t_ccode_r大括号将收集全部代码(使用前面记录的起始位置),存储它,并返回一个包含全部文本的令牌“CCODE”。当返回令牌时,词法分析状态将恢复到初始状态。
lexer要求以单个输入字符串的形式提供输入。因为大多数机器都有足够的内存,所以不多会出现性能问题。可是,这意味着lexer目前不能用于流数据,例如打开的文件或套接字。这种限制主要是使用re模块的反作用。您能够经过实现适当的def t_eof()文件末尾处理规则来解决这个问题。这里的主要复杂之处在于,您可能须要确保以某种方式将数据提供给lexer,这样它就不会在令牌中间分裂。
lexer应该可以正确地处理做为令牌和模式匹配规则给出的Unicode字符串以及输入文本。
您须要向re.compile()函数提供可选的标志,使用lex的reflags选项。例如:
lex.lex(reflags=re.UNICODE | re.VERBOSE)
默认状况下,reflags被设置为re.VERBOSE。若是您提供了本身的标志,您可能须要将其包含在PLY中以保持其正常行为。
因为lexer彻底是用Python编写的,因此它的性能在很大程度上取决于Python re模块的性能。尽管lexer被编写得尽量的高效,可是当它被用于很是大的输入文件时,它的速度并不快。若是关心性能,能够考虑升级到Python的最新版本,建立一个手写的lexer,或者将lexer卸载到C扩展模块中。
若是您打算建立一个手写的lexer,并计划与yacc一块儿使用它。,只须要符合如下要求:
yacc.py用于解析语言的语法,在展现示例以前,必须提到一些重要的背景知识。首先,语法一般用BNF语法指定。例如,若是您想解析简单的算术表达式,您能够首先编写一个明确的语法规范,以下所示:
expression : expression + term | expression - term | term term : term * factor | term / factor | factor factor : NUMBER | ( expression )
在语法中,数字、+、-、*和/等符号被称为终结符,与原始输入标记相对应。
诸如term和factor之类的标识符是指由一组终结符和其余规则组成的语法规则,这些被称之为非终结符。
语言的语义行为一般使用一种称为语法制导翻译的技术来指定。在语法制导翻译中,属性与操做一块儿附加到给定语法规则中的每一个符号。只要识别出特定的语法规则,动做就描述要作什么。例如,给定上面的表达式语法,您能够编写一个像这样的简单计算器的规范:
理解语法制导翻译的一个方式是将语法中的每一个符号看作是一个对象,与每一个符号相关联的是一个表示其“状态”的值,而后,语义动做被表示为操做符号和相关值的函数或方法的集合。
Yacc使用的解析技术,称为LR解析或shift-reduce解析,LR解析是一种自下而上的技术。
LR解析经过栈来操做,下面有一个用于解析 3 + 5 * (10 - 20)
的栈操做,其中$表示输入的结束。
若是栈中顶部元素能够合并成生成式左边的表达式,则能够进行合并。解析的成功取决于最终栈中元素为空且没有新输入的token。
ply.yacc
模块实现了PLY中的内容解析。Yacc表明"Yet Another Compiler Compiler" ,采用了和Unix中同样的名称。
若是你想要作一个简单的语法表达式的解析,下面有一个简单的例子:
# Yacc example import ply.yacc as yacc # Get the token map from the lexer. This is required. from calclex import tokens def p_expression_plus(p): 'expression : expression PLUS term' p[0] = p[1] + p[3] def p_expression_minus(p): 'expression : expression MINUS term' p[0] = p[1] - p[3] def p_expression_term(p): 'expression : term' p[0] = p[1] def p_term_times(p): 'term : term TIMES factor' p[0] = p[1] * p[3] def p_term_div(p): 'term : term DIVIDE factor' p[0] = p[1] / p[3] def p_term_factor(p): 'term : factor' p[0] = p[1] def p_factor_num(p): 'factor : NUMBER' p[0] = p[1] def p_factor_expr(p): 'factor : LPAREN expression RPAREN' p[0] = p[2] # Error rule for syntax errors def p_error(p): print("Syntax error in input!") # Build the parser parser = yacc.yacc() while True: try: s = raw_input('calc > ') except EOFError: break if not s: continue result = parser.parse(s) print(result)
在上述例子中,每个语法规则规定义为一个python的函数,每一个函数接收一个参数p,其中p为一个包含每一个语法符号对应值的序列。p[i]的值是一个语法符号的映射,如:
def p_expression_plus(p): 'expression : expression PLUS term' # ^ ^ ^ ^ # p[0] p[1] p[2] p[3] p[0] = p[1] + p[3]
对应token来讲,p[i]的值相似于在词法分析中的属性值 p.value
。对于非终端结点而言,当进行规约的时候,该值取决于p[0]中放置的内容决定。这个值能够是任何值。然而,最多见的值多是简单的Python类型、元组或实例。在本例中,咱们依赖于NUMBER在其值字段中存储整数值。全部其余规则只是执行各类类型的整数操做并传播结果。
注意:负索引的使用在yacc中有特殊的意义——在本例中,特殊的p[-1]与p[3]没有相同的值。有关详细信息,请参阅“嵌入式操做”一节。
yacc规范中定义的第一个规则肯定开始语法符号(在本例中,首先出现expression规则)。每当解析器规约了起始规则,而且没有更多的输入可用时,解析就会中止,并返回最终的值(这个值将是p[0]中放置的最上面的规则)。注意:可使用yacc()的start关键字参数start指定另外一个启动符号。
定义p_error§规则是为了捕获语法错误。有关详细信息,请参阅下面的错误处理部分。
要构建解析器,请调用yacc.yacc()函数。这个函数查看模块并尝试为您指定的语法构造全部LR解析表。第一次调用yacc.yacc()时,您将获得这样一条消息:
$ python calcparse.py Generating LALR tables calc >
因为表的构造相对比较昂贵(特别是对于大型语法),所以产生的解析表被写到一个名为parsetab.py的文件中。此外,还有一个名为parser.out的调试文件建立。在随后的执行中,yacc将从parsetab.py从新加载表,除非它检测到底层语法的变化(在这种状况下,表和parsetab.py文件将从新生成)。这两个文件都被写到与指定解析器的模块相同的目录中。可使用tabmodule关键字参数yacc()更改parsetab模块的名称。例如:
parser = yacc.yacc(tabmodule='fooparsetab')
若是在语法规范中检测到任何错误,yacc.py将生成诊断消息,并可能引起异常。能够检测到的错误包括:
接下来的几节将更详细地讨论语法规范。示例的最后一部分显示了如何实际运行yacc()建立的解析器。要运行解析器,只需使用输入文本字符串调用parse()。这将运行全部语法规则并返回整个解析的结果。这个结果返回值是在开始语法规则中分配给p[0]的值。
当语法规则类似时,能够将它们组合成一个函数。例如,考虑前面例子中的两条规则:
def p_expression_plus(p): 'expression : expression PLUS term' p[0] = p[1] + p[3] def p_expression_minus(t): 'expression : expression MINUS term' p[0] = p[1] - p[3]
能够写成一个函数,以下所示:
def p_expression(p): '''expression : expression PLUS term | expression MINUS term''' if p[2] == '+': p[0] = p[1] + p[3] elif p[2] == '-': p[0] = p[1] - p[3]
一般,任何给定函数的字符串均可以包含多个语法规则。因此,这样写也是合法的(尽管可能会让人困惑):
def p_binary_operators(p): '''expression : expression PLUS term | expression MINUS term term : term TIMES factor | term DIVIDE factor''' if p[2] == '+': p[0] = p[1] + p[3] elif p[2] == '-': p[0] = p[1] - p[3] elif p[2] == '*': p[0] = p[1] * p[3] elif p[2] == '/': p[0] = p[1] / p[3]
当将语法规则组合成单个函数时,一般全部规则都具备相似的结构(例如,相同数量的术语)。不然,相应的操做代码可能比须要的更复杂。可是,可使用len()处理简单的状况。例如:
def p_expressions(p): '''expression : expression MINUS expression | MINUS expression''' if (len(p) == 4): p[0] = p[1] - p[3] elif (len(p) == 3): p[0] = -p[2]
解析性能是一个须要考虑的问题,您应该克制将太多条件处理放入单个语法规则的冲动,如这些示例所示。当您添加检查以查看正在处理的语法规则时,其实是在复制解析器已经执行的工做(即,解析器已经确切地知道它匹配的规则)。您能够经过为每一个语法规则使用单独的p_rule()函数来消除这种开销。
若是须要,语法能够包含定义为单个字符文字的标记。例如:
def p_binary_operators(p): '''expression : expression '+' term | expression '-' term term : term '*' factor | term '/' factor''' if p[2] == '+': p[0] = p[1] + p[3] elif p[2] == '-': p[0] = p[1] - p[3] elif p[2] == '*': p[0] = p[1] * p[3] elif p[2] == '/': p[0] = p[1] / p[3]
字符文字必须用引号括起来,如“+”。此外,若是使用了文本,则必须经过使用特殊的文本声明在相应的lex文件中声明它们。
# Literals. Should be placed in module given to lex() literals = ['+','-','*','/' ]
字符文字仅限于单个字符。所以,指定诸如’<=‘或’==‘之类的文字是不合法的。为此,使用常规的词法规则(例如,定义一个规则,如t_EQ = r’==’)。
yacc.py能够经过定义以下规则来处理空结果:
def p_empty(p): 'empty :' pass
使用空结果能够直接使用“empty”做为符号,例如:
def p_optitem(p): 'optitem : item' ' | empty' ...
注意:您能够在任何地方编写空规则,只需指定右侧为空便可。然而,我我的发现,写一个“空”规则并用“空”来表示一个空的结果更容易阅读,也更清楚地说明了您的意图。
一般,在yacc规范中发现的第一个规则定义了开始语法规则(顶级规则)。要更改这一点,只需在文件中提供一个start说明符。例如:
start = 'foo' def p_bar(p): 'bar : A B' # This is the starting rule due to the start specifier above def p_foo(p): 'foo : bar X' ...
在调试过程当中使用start说明符可能颇有用,由于您可使用它来让yacc构建更大语法的子集。为此,也能够指定起始符号做为yacc()的参数。例如:
parser = yacc.yacc(start='foo')
前面的例子中给出的表达式语法是用一种特殊的格式编写的,以消除歧义。然而,在许多状况下,用这种格式编写语法极其困难或尴尬。一种更天然的表达语法的方式是这样一种更紧凑的形式:
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”和内部解析堆栈:
在本例中,当解析器达到步骤6时,它有两个选项。一种是减小堆栈上的规则expr: expr * expr。另外一个选项是在堆栈上移入”+“。根据上下文无关语法的规则,这两个选项都是彻底合法的。
默认状况下,全部的移入/规约冲突都是经过移入来解决的。所以,在上面的例子中,解析器老是将+移位而不是减小。虽然这种策略在不少状况下都有效(例如,“if-then”与“if-then-else”的状况),可是对于算术表达式来讲还不够。事实上,在上面的例子中,移位+的决定是彻底错误的——咱们应该减小expr * expr,由于乘法比加法具备更高的数学优先级。
为了解决歧义,特别是在表达式语法中,yacc.py容许为单个标记分配优先级和关联性。这是经过向语法文件添加一个变量优先级来实现的,以下所示:
precedence = ( ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'), )
这个声明指定加减具备相同的优先级,而且是左关联的,而乘以/除以具备相同的优先级,而且是左关联的。在优先声明中,token从最低优先级排序到最高优先级。所以,这个声明指定TIMES/DIVIDE的优先级高于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)
当遇到移位/规约冲突时,解析器生成器经过查看优先规则和关联说明符来解决冲突。
例如,当"expression PLUS expression" 遇到下一个"TIMES",则应该进行移入操做。相反,"expression TIMES expression"遇到““PLUS””则进行规约操做。
优先说明符技术的一个问题是,有时须要在某些上下文中更改操做符的优先级。例如,考虑“3 + 4 * -5”中的一元减运算符。
从数学上讲,一元减号一般具备很高的优先级——在乘法以前求值。然而,在咱们的优先说明符中,-的优先级比TIMES低。为了处理这个问题,能够为所谓的“虚拟令牌”提供优先规则,以下所示:
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覆盖默认规则优先级——在优先说明符中将其设置为UMINUS。
当有多个语法规则可应用于给定的一组符号时,会致使Reduce/ Reduce冲突。这种冲突几乎老是很差的,老是经过选择语法文件中首先出现的规则来解决。当不一样的语法规则以某种方式生成相同的符号集时,几乎老是会引发Reduce/ Reduce冲突。例如:
在这种状况下,这两条规则之间存在一个reduce/reduce冲突:
assignment : ID EQUALS NUMBER expression : NUMBER
例如,若是你写了“a=5”,解析的时候将不清楚究竟是解析为assignment : ID EQUALS NUMBER
仍是 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
的内容。
跟踪shift/reduce和reduce/reduce冲突是使用LR解析算法的一个更好的乐趣。为了帮助调试,yacc.py建立了一个名为“parser”的调试文件。当它生成解析表时。该文件的内容以下:
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
该文件中出现的不一样状态表示语法容许的每一个可能的有效输入标记序列。当接收输入标记时,解析器将构建一个堆栈并寻找匹配的规则。每一个状态都跟踪可能正在匹配的语法规则。在每一个规则中,“.”字符表示该规则中解析的当前位置。此外,还列出了每一个有效输入token的操做。当发生移位/规约或规约/规约冲突时,未选择的规则前面加上一个“!”。例如:
! TIMES [ reduce using rule 2 ] ! DIVIDE [ reduce using rule 2 ] ! PLUS [ shift and go to state 6 ] ! MINUS [ shift and go to state 5 ]
经过查看这些规则(并进行一些实践),您一般能够找到大多数解析冲突的根源。还应该强调的是,并非全部的shift-reduce冲突都是很差的。可是,确保正确解析它们的惟一方法是查看parser.out。
若是您正在建立一个供生产使用的解析器,那么语法错误的处理是很是重要的。通常来讲,您不但愿解析器在出现问题的第一个迹象时就举手投降。相反,您但愿它报告错误,若是可能的话进行恢复,并继续解析,以便当即将输入中的全部错误报告给用户。这是在C、c++和Java等语言的编译器中发现的标准行为。
一般,当语法错误在解析过程当中发生时,会当即检测到该错误(即,解析器只读取错误源以外的任何标记)。可是,此时,解析器进入了一个恢复模式,可使用该模式尝试并继续进一步解析。通常来讲,LR解析器中的错误恢复是一个古老话题。yacc.py提供的恢复机制能够与Unix yacc相媲美,所以您可能想要查阅O’Reilly的《Lex and yacc》之类的书以得到更详细的信息。
当出现语法错误时,yacc.py执行如下步骤:
编写编译器时,位置跟踪经常是一个棘手的问题。默认状况下,PLY跟踪全部token的行号和位置。这些资料可用如下功能提供:
p.lineno(num)
. 返回行号p.lexpos(num)
. 返回相对于文本的相对位置例如:
def p_expression(p): 'expression : expression PLUS expression' line = p.lineno(2) # line number of the PLUS token index = p.lexpos(2) # Position of the PLUS token
做为一个可选特性,yacc.py还能够自动跟踪全部语法符号的行号和位置。可是,这种额外的跟踪须要额外的处理,而且会显著下降解析速度。所以,必须经过将tracking=True选项传递给yacc.parse()来启用它。例如:
yacc.parse(data,tracking=True)
一旦启用,lineno()和lexpos()方法就能够用于全部语法符号。此外,还可使用另外两种方法:
p.linespan(num)
. 返回一个元组(起始行、结束行)p.lexspan(num)
. 返回一个元组(start,end),表示起始结束位置。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能够方便地跟踪全部语法符号的位置信息,但这一般是没必要要的。例如,若是您只是在错误消息中使用行号信息,那么您一般能够在语法规则中键入特定的token。例如:
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不保留已解析规则中的行号信息。若是您正在构建一个抽象语法树,而且须要行号,那么您应该确保行号出如今树自己中。
yacc.py
并无提供特殊的方法来构造AST,然而这种构造能够本身来轻松实现。
构造树的最简单方法是在每一个语法规则函数中建立和传播一个元组或列表。有不少方法能够作到这一点,其中一个例子是这样的:
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])
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符号左侧的符号。在这种状况下,它将是上面foo规则中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 :"
将引入一个额外的shift-reduce冲突。这个冲突是因为相同的符号C出如今abcd和abcx规则中。解析器能够移动符号(abcd规则)或规约空规则seen_AB (abcx规则)。
嵌入式规则的一个常见用途是控制解析的其余方面,好比局部变量的范围。例如,若是您正在解析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) ...
在这种状况下,在解析LBRACE({)符号以后,嵌入的动做new_scope当即执行。这可能会调整内部符号表和解析器的其余方面。在规则statements_block完成后,代码能够撤消在嵌入式操做(例如,pop_scope())中执行的操做。
默认状况下,yacc.py依赖于lex.py进行标记。可是,能够提供另外一种token,以下:
parser = yacc.parse(lexer=x)
在本例中,x必须是Lexer对象,该对象至少具备用于检索下一个token的x.token()方法。若是向yacc.parse()提供输入字符串,lexer还必须有一个x.input()方法。
默认状况下,yacc以调试模式生成表(调试模式生成解析器)。输出文件和其余输出)。若要禁用此功能,请使用
parser = yacc.yacc(debug=False)
要更改parsetab.py文件的名称,请使用:
parser = yacc.yacc(tabmodule="foo")
一般,parsetab.py文件与定义解析器的模块放在同一个目录中。若是您想将它放到其余地方,您能够为tabmodule提供一个绝对的包名。在这种状况下,表将写在那里。
要更改写入parsetab.py文件(和其余输出文件)的目录,请使用:
parser = yacc.yacc(tabmodule="foo",outputdir="somedirectory")
注意:除非指定的目录也位于Python的路径上(sys.path),不然后续的表文件导入将失败。通常来讲,最好使用tabmodule参数指定目标,而不是直接使用outputdir参数指定目录。
要防止yacc生成任何类型的解析器表文件,请使用:
parser = yacc.yacc(write_tables=False)
注意:若是禁用表生成,yacc()将在每次运行时从新生成解析表(这可能须要一段时间,这取决于语法的大小)。
若要在解析期间打印大量调试,请使用:
parser.parse(input_text, debug=True)
因为生成LALR表的成本相对较高,所以之前生成的表将被缓存并尽量重用。从新生成表的决定是经过对全部语法规则和优先规则进行MD5校验和来肯定的。只有在不匹配的状况下才会从新生成表。
应该注意的是,表生成至关高效,即便对于涉及大约100条规则和几百种状态的语法也是如此。
因为LR解析是由表驱动的,因此解析器的性能在很大程度上独立于语法的大小。最大的瓶颈将是lexer和语法规则中代码的复杂性。
yacc()还容许将解析器定义为类和闭包(请参阅关于lexer的替代规范的部分)。可是,请注意在单个模块(源文件)中只能定义一个解析器。若是您试图在同一个源文件中定义多个解析器,可能会出现各类错误检查和验证步骤,从而产生混淆的错误消息。
生产规则的装饰器必须更新包装函数的行号。wrapper.co_firstlineno = func.__code__.co_firstlineno
:
from functools import wraps from nodes import Collection def strict(*types): def decorate(func): @wraps(func) def wrapper(p): func(p) if not isinstance(p[0], types): raise TypeError wrapper.co_firstlineno = func.__code__.co_firstlineno return wrapper return decorate @strict(Collection) def p_collection(p): """ collection : sequence | map """ p[0] = p[1]
在高级解析应用程序中,您可能但愿拥有多个解析器和lexer。
通常来讲,这不是问题。然而,要使它工做,您须要仔细确保全部内容都正确地链接起来。首先,确保保存lex()和yacc()返回的对象。例如:
lexer = lex.lex() # Return lexer object parser = yacc.yacc() # Return parser object
接下来,在解析时,确保将parse()函数引用到它应该使用的lexer。例如:
parser.parse(text,lexer=lexer)
若是忘记这样作,解析器将使用最后建立的lexer——这并不老是您想要的。
在lexer和parser规则函数中,这些对象也是可用的。在lexer中,令牌的“lexer”属性引用触发规则的lexer对象。例如:
def t_NUMBER(t): r'\d+' ... print(t.lexer) # Show lexer object
在解析器中,“lexer”和“parser”属性分别引用lexer和parser对象。
def p_expr_plus(p): 'expr : expr PLUS expr' ... print(p.parser) # Show parser object print(p.lexer) # Show lexer object
若是须要,能够将任意属性附加到lexer或解析器对象。例如,若是但愿有不一样的解析模式,能够将模式属性附加到解析器对象上,稍后再进行研究。
由于PLY使用来自doc-string的信息,因此在正常模式下运行Python解释器(即,而不是-O或-OO选项)。可是,若是您像这样指定优化模式:
lex.lex(optimize=1) yacc.yacc(optimize=1)
而后,当Python以优化模式运行时,可使用PLY。要使此工做正常,请确保首先在正常模式下运行Python。第一次生成词法分析和解析表以后,以优化模式运行Python。PLY将使用不须要doc字符串的表。
注意:在优化模式下运行PLY会禁用大量错误检查。您应该只在项目已稳定且不须要进行任何调试时才这样作。优化模式的目的之一是显著减小编译器的启动时间(假设全部内容都已正确指定并正常工做)。
调试编译器一般不是一项容易的任务。PLY经过使用Python的日志模块提供了一些高级的对角线功能。下面两部分将对此进行描述:
lex()和yacc()命令都具备可使用debug标志启用的调试模式。例如:
lex.lex(debug=True) yacc.yacc(debug=True)
一般,调试生成的输出要么路由到标准错误,要么(在yacc()的状况下)路由到文件parser.out。经过提供一个日志对象,能够更仔细地控制这个输出。下面是一个例子,它添加了关于不一样调试消息来自何处的信息:
# 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)
若是提供自定义日志记录器,则能够经过设置日志记录级别来控制生成的调试信息的数量。一般,调试消息在调试、信息或警告级别发出。
PLY的错误消息和警告也使用日志记录接口生成。这能够经过使用errorlog参数传递日志对象来控制。
lex.lex(errorlog=log) yacc.yacc(errorlog=log)
若是但愿彻底消除警告,能够传入具备适当过滤器级别的日志对象,或者使用lex或yacc中定义的NullLogger对象。例如:
yacc.yacc(errorlog=yacc.NullLogger())
若要启用解析器的运行时调试,请使用debug选项进行解析。这个选项能够是整数(它只是打开或关闭调试),也能够是logger对象的实例。例如:
log = logging.getLogger() parser.parse(input,debug=log)
若是传递了日志对象,则可使用其筛选级别来控制生成了多少输出。INFO级别用于生成关于规则缩减的信息。调试级别将显示有关解析堆栈、令牌转换和其余细节的信息。错误级别显示与解析错误相关的信息。
对于很是复杂的问题,您应该传递一个日志对象,该对象将重定向到一个文件,在执行后,您能够更容易地检查输出。
若是您正在分发一个使用PLY的包,您应该花一些时间考虑如何处理自动生成的文件。例如,yacc()函数生成的parsetab.py文件。
在PLY-3.6中,表文件建立在与定义解析器的文件相同的目录中。这意味着parsetab.py文件将与解析器规范共存。就打包而言,这多是最简单、最理智的管理方法。您不须要给yacc()任何额外的参数,它应该只是“工做”。
一个关注点是parsetab.py文件自己的管理。例如,您应该将该文件签入版本控制(例如GitHub),它应该做为普通文件包含在包分发版中,仍是应该让PLY在用户安装您的包时自动生成它?
从PLY -3.6开始,parsetab.py文件应该兼容全部Python版本,包括Python 2和Python 3。所以,若是在python3上使用,用python2生成的表文件应该能够正常工做。所以,若是须要的话,本身分发parsetab.py文件应该是相对无害的。可是,请注意,若是未来对文件的格式进行了加强或更改,那么PLY的旧/新版本可能会尝试从新生成文件。
为了使表文件的生成更易于安装,您可使用-m选项或相似的方法使解析器文件可执行。例如:
# calc.py ... ... def make_parser(): parser = yacc.yacc() return parser if __name__ == '__main__': make_parser()
而后可使用python -m calc .py之类的命令生成表。另外,setup.py脚本能够导入模块并使用make_parser()建立解析表。
若是愿意牺牲一点启动时间,还能够指示PLY不要使用yacc编写表。yacc.yacc(write_tables=False, debug=False)。在此模式下,PLY将每次从新生成解析表。对于一个小语法,您可能不会注意到。对于大型语法,您可能应该从新考虑—解析表的目的是显著加快这个过程。
在操做过程当中,正常状况是PLY生成诊断错误消息(一般打印为标准错误)。这些都是彻底使用日志模块生成的。若是但愿重定向这些消息或使其保持静默,能够将本身的日志对象提供给yacc()。例如:
import logging log = logging.getLogger('ply') ... parser = yacc.yacc(errorlog=log)
PLY分布的examples目录包含几个简单的示例。有关理论和底层实现细节或LR解析,请参阅编译器教科书。