PLY (Python Lex-Yacc)
本文档提供了使用PLY进行词法分析和解析的概述,考虑到解析的内在复杂性,我强烈建议您在使用PLY进行大型开发项目之前阅读(或至少略读)整个文档。
PLY是流行的编译器构造工具lex和yacc的纯python实现。PLY的主要目标是相当忠实于传统lex/yacc工具的工作方式。这包括支持LALR(1)解析,以及提供广泛的输入验证、错误报告和诊断。因此,如果您在另一种编程语言中使用过yacc,那么使用PLY应该相对简单。
PLY的早期版本是为了支持David 2001年在芝加哥大学(University of Chicago)教授的编译器入门课程而开发的。由于PLY最初是作为教学工具开发的,所以您会发现它对标记和语法规则规范相当挑剔。在某种程度上,这种附加的形式是为了捕捉新手用户所犯的常见编程错误。然而,高级用户在为真正的编程语言构建复杂语法时也会发现这些特性非常有用。还应该注意,PLY没有提供太多额外功能(例如,自动构造抽象语法树、遍历树等)。我也不认为它是一个解析框架。相反,您将发现一个完全用Python编写的、功能齐全的基本lex/yacc实现。
本文的其余部分需要假设您对解析理论、语法制导翻译以及其他编程语言中编译器构造工具(如lex和yacc)的使用有一定的了解。如果您不熟悉这些主题,您可能想要查阅介绍性的文本,例如Aho、Sethi和Ullman编写的“编译器:原则、技术和工具”。O’Reilly的John Levine的《Lex and Yacc》可能也很方便。事实上,O’Reilly的书可以作为PLY的参考,因为这两个概念实际上是相同的。
PLY由两个单独的模块组成:lex.py 和 yacc. p y。都可以在名为ply的Python包中找到。
lex.py模块用于将输入的文本通过正则表达式转化成一系列特定的token。
yacc.py用于识别以上下文无关语法形式指定的语言语法。
这两种工具应该一起工作。具体来说,lex.py以token()函数的形式提供了一个外部接口,该函数返回输入流上的下一个有效token。yacc.py用于反复调用函数来检索标记并调用语法规则。yacc.py的输出通常是抽象语法树(AST)。然而,这完全取决于用户。如果需要,还可以使用yacc.py实现简单的单遍编译器。
与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解析,请参阅编译器教科书。