ply 教程

https://www.dabeaz.com/ply/ply.html#ply_nn28b
https://www.kancloud.cn/kancloud/ply/42120
http://www.tastones.com/stackoverflow/python-language/python-lex-yacc/getting_started_with_ply/



https://www.ctolib.com/docs-python-lex-yacc-c-ply-03-summary.html



https://github.com/dabeaz/ply/blob/master/setup.md



https://github.com/dabeaz/ply



https://blog.csdn.net/zuzhiang/article/details/79047743
PLY 包含两个独立的模块:lex.py 和 yacc.py,都定义在 ply 包下。lex.py 模块用来将输入字符通过一系列的正则表达式分解成标记序列,yacc.py 通过一些上下文无关的文法来识别编程语言语法。yacc.py 使用 LR 解析法,并使用 LALR(1)算法(默认)或者 SLR 算法生成分析表。



这两个工具是为了一起工作的。lex.py 通过向外部提供token()方法作为接口,方法每次会从输入中返回下一个有效的标记。yacc.py 将会不断的调用这个方法来获取标记并匹配语法规则。yacc.py 的功能通常是生成抽象语法树(AST),不过,这完全取决于用户,如果需要,yacc.py 可以直接用来完成简单的翻译。



就像相应的 unix 工具,yacc.py 提供了大多数你期望的特性,其中包括:丰富的错误检查、语法验证、支持空产生式、错误的标记、通过优先级规则解决二义性。事实上,传统 yacc 能够做到的 PLY 都应该支持。



yacc.py 与 Unix 下的 yacc 的主要不同之处在于,yacc.py 没有包含一个独立的代码生成器,而是在 PLY 中依赖反射来构建词法分析器和语法解析器。不像传统的 lex/yacc 工具需要一个独立的输入文件,并将之转化成一个源文件,Python 程序必须是一个可直接可用的程序,这意味着不能有额外的源文件和特殊的创建步骤(像是那种执行 yacc 命令来生成 Python 代码)。又由于生成分析表开销较大,PLY 会缓存生成的分析表,并将它们保存在独立的文件中,除非源文件有变化,会重新生成分析表,否则将从缓存中直接读取。



由于 PLY 是作为教学工具来开发的,你会发现它对于标记和语法规则是相当严谨的,这一定程度上是为了帮助新手用户找出常见的编程错误。不过,高级用户也会发现这有助于处理真实编程语言的复杂语法。还需要注意的是,PLY 没有提供太多花哨的东西(例如,自动构建抽象语法树和遍历树),我也不认为它是个分析框架。相反,你会发现它是一个用 Python 实现的,基本的,但能够完全胜任的 lex/yacc。



本文的假设你多少熟悉分析理论、语法制导的翻译、基于其他编程语言使用过类似 lex 和 yacc 的编译器构建工具。如果你对这些东西不熟悉,你可能需要先去一些书籍中学习一些基础,比如:Aho, Sethi 和 Ullman 的《Compilers: Principles, Techniques, and Tools》(《编译原理》),和 O’Reilly’ 出版的J ohn Levine 的《lex and yacc》。事实上,《lex and yacc》和 PLY 使用的概念几乎相同。



Build the lexer


lexer = lex.lex()
由 lexer.token()方法返回的标记是 LexToken 类型的实例,拥有tok.type,tok.value,tok.lineno和tok.lexpos属性



标记列表
词法分析器必须提供一个标记的列表,这个列表将所有可能的标记告诉分析器,用来执行各种验证,同时也提供给 yacc.py 作为终结符。



标记的规则
每种标记用一个正则表达式规则来表示,每个规则需要以”t_“开头声明,表示该声明是对标记的规则定义。对于简单的标记,可以定义成这样(在 Python 中使用 raw string 能比较方便的书写正则表达式):



t_PLUS = r’+’
这里,紧跟在 t_ 后面的单词,必须跟标记列表中的某个标记名称对应。如果需要执行动作的话,规则可以写成一个方法。例如,下面的规则匹配数字字串,并且将匹配的字符串转化成 Python 的整型:



def t_NUMBER(t):
r’\d+’
t.value = int(t.value)
return t



如果使用方法的话,正则表达式写成方法的文档字符串。方法总是需要接受一个 LexToken 实例的参数,该实例有一个 t.type 的属性(字符串表示)来表示标记的类型名称,t.value 是标记值(匹配的实际的字符串),t.lineno 表示当前在源输入串中的作业行,t.lexpos 表示标记相对于输入串起始位置的偏移。默认情况下,t.type 是以t_开头的变量或方法的后面部分。方法可以在方法体里面修改这些属性。但是,如果这样做,应该返回结果 token,否则,标记将被丢弃。



在 lex 内部,lex.py 用re模块处理模式匹配,在构造最终的完整的正则式的时候,用户提供的规则按照下面的顺序加入:



所有由方法定义的标记规则,按照他们的出现顺序依次加入
由字符串变量定义的标记规则按照其正则式长度倒序后,依次加入(长的先入)
顺序的约定对于精确匹配是必要的。比如,如果你想区分‘=’和‘==’,你需要确保‘==’优先检查。如果用字符串来定义这样的表达式的话,通过将较长的正则式先加入,可以帮助解决这个问题。用方法定义标记,可以显示地控制哪个规则优先检查。



为了处理保留字,你应该写一个单一的规则来匹配这些标识,并在方法里面作特殊的查询:



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 返回后,它们的值被保存在value属性中。正常情况下,value 是匹配的实际文本。事实上,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属 性,访问其他属性会变得不自然。如果想保存多种属性,可以将元组、字典、或者对象实例赋给 value。



丢弃标记
想丢弃像注释之类的标记,只要不返回 value 就行了,像这样:



def t_COMMENT(t):
r’#.*’
pass
# No return value. Token discarded
为标记声明添加”ignore_“前缀同样可以达到目的:



t_ignore_COMMENT = r’#.*’
如果有多种文本需要丢弃,建议使用方法来定义规则,因为方法能够提供更精确的匹配优先级控制(方法根据出现的顺序,而字符串的正则表达式依据正则表达式的长度)



行号和位置信息
默认情况下,lex.py 对行号一无所知。因为 lex.py 根本不知道何为”行”的概念(换行符本身也作为文本的一部分)。不过,可以通过写一个特殊的规则来记录行号:



Define a rule so we can track line numbers


def t_newline(t):
r’\n+’
t.lexer.lineno += len(t.value)
在这个规则中,当前 lexer 对象 t.lexer 的 lineno 属性被修改了,而且空行被简单的丢弃了,因为没有任何的返回。



lex.py 也不自动做列跟踪。但是,位置信息被记录在了每个标记对象的lexpos属性中,这样,就有可能来计算列信息了。例如:每当遇到新行的时候就重置列值:



Compute column.


input is the input text string


token is a token instance


def find_column(input,token):
last_cr = input.rfind(‘\n’,0,token.lexpos)
if last_cr < 0:
last_cr = 0
column = (token.lexpos - last_cr) + 1
return column
通常,计算列的信息是为了指示上下文的错误位置,所以只在必要时有用。



忽略字符
t_ignore规则比较特殊,是lex.py所保留用来忽略字符的,通常用来跳过空白或者不需要的字符。虽然可以通过定义像t_newline()这样的规则来完成相同的事情,不过使用t_ignore能够提供较好的词法分析性能,因为相比普通的正则式,它被特殊化处理了。



字面字符
字面字符可以通过在词法模块中定义一个literals变量做到,例如:



literals = [ ‘+’,’-‘,’*’,’/’ ]
或者



literals = “+-*/”
字面字符是指单个字符,表示把字符本身作为标记,标记的type和value都是字符本身。不过,字面字符是在其他正则式之后被检查的,因此如果有规则是以这些字符开头的,那么这些规则的优先级较高。



错误处理
最后,在词法分析中遇到非法字符时,t_error()用来处理这类错误。这种情况下,t.value包含了余下还未被处理的输入字串,在之前的例子中,错误处理方法是这样的:



Error handling rule


def t_error(t):
print “Illegal character ‘%s’” % t.value[0]
t.lexer.skip(1)
这个例子中,我们只是简单的输出不合法的字符,并且通过调用t.lexer.skip(1)跳过一个字符。



构建和使用 lexer
函数lex.lex()使用 Python 的反射机制读取调用上下文中的正则表达式,来创建 lexer。lexer 一旦创建好,有两个方法可以用来控制 lexer 对象:



lexer.input(data) 重置 lexer 和输入字串
lexer.token() 返回下一个 LexToken 类型的标记实例,如果进行到输入字串的尾部时将返回None
推荐直接在 lex() 函数返回的 lexer 对象上调用上述接口,尽管也可以向下面这样用模块级别的 lex.input() 和 lex.token():



lex.lex()
lex.input(sometext)
while 1:
tok = lex.token()
if not tok: break
print tok
在这个例子中,lex.input() 和 lex.token() 是模块级别的方法,在 lex 模块中,input() 和 token() 方法绑定到最新创建的 lexer 对象的对应方法上。最好不要这样用,因为这种接口可能不知道在什么时候就失效(译者注:垃圾回收?)



@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):

装饰器可以将 identifier 关联到 t_ID() 的文档字符串上以使 lex.py 正常工作,一种等价的做法是直接给文档字符串赋值:



def t_ID(t):



t_ID.doc = identifier
注意:@TOKEN 装饰器需要 Python-2.4 以上的版本。如果你在意老版本Python的兼容性问题,使用上面的等价办法。



优化模式
为了提高性能,你可能希望使用 Python 的优化模式(比如,使用 -o 选项执行 Python)。然而,这样的话,Python 会忽略文档字串,这是 lex.py 的特殊问题,可以通过在创建 lexer 的时候使用 optimize 选项:



lexer = lex.lex(optimize=1)
接着,用 Python 常规的模式运行,这样,lex.py 会在当前目录下创建一个 lextab.py 文件,这个文件会包含所有的正则表达式规则和词法分析阶段的分析表。然后,lextab.py 可以被导入用来构建 lexer。这种方法大大改善了词法分析程序的启动时间,而且可以在 Python 的优化模式下工作。



想要更改生成的文件名,使用如下参数:



lexer = lex.lex(optimize=1,lextab=”footab”)
在优化模式下执行,需要注意的是 lex 会被禁用大多数的错误检查。因此,建议只在确保万事俱备准备发布最终代码时使用。



调试
如果想要调试,可以使 lex() 运行在调试模式:



lexer = lex.lex(debug=1)
这将打出一些调试信息,包括添加的规则、最终的正则表达式和词法分析过程中得到的标记。



除此之外,lex.py 有一个简单的主函数,不但支持对命令行参数输入的字串进行扫描,还支持命令行参数指定的文件名:



if name == ‘main’:
lex.runmain()
想要了解高级调试的详情,请移步至最后的高级调试部分。



其他方式定义词法规则
上面的例子,词法分析器都是在单个的 Python 模块中指定的。如果你想将标记的规则放到不同的模块,使用 module 关键字参数。例如,你可能有一个专有的模块,包含了标记的规则:



module: tokrules.py


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)
现在,如果你想要从不同的模块中构建分析器,应该这样(在交互模式下):






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
module选项也可以指定类型的实例,例如:






import ply.lex as lex



class MyLexer:
# 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 = 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 的方法只有被绑定(bound-methods)对象后才能使 PLY 正常工作。



当给 lex() 方法使用 module 选项时,PLY 使用dir()方法,从对象中获取符号信息,因为不能直接访问对象的__dict__属性。(译者注:可能是因为兼容性原因,dict这个方法可能不存在)



最后,如果你希望保持较好的封装性,但不希望什么东西都写在类里面,lexers 可以在闭包中定义,例如:



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() 额外状态维护 在你的词法分析器中,你可能想要维护一些状态。这可能包括模式设置,符号表和其他细节。例如,假设你想要跟踪NUMBER标记的出现个数。


一种方法是维护一个全局变量:



num_count = 0
def t_NUMBER(t):
r’\d+’
global num_count
num_count += 1
t.value = int(t.value)

return t
如果你不喜欢全局变量,另一个记录信息的地方是 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 实例的情况下,简单易行。不过这看上去似乎是严重违反了面向对象的封装原则。lexer 的内部属性(除了 lineno )都是以 lex 开头命名的(lexdata、lexpos)。因此,只要不以 lex 开头来命名属性就很安全的。



如果你不喜欢给 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

Lexer 克隆
如果有必要的话,lexer 对象可以通过clone()方法来复制:



lexer = lex.lex()

newlexer = lexer.clone()
当 lexer 被克隆后,复制品能够精确的保留输入串和内部状态,不过,新的 lexer 可以接受一个不同的输出字串,并独立运作起来。这在几种情况下也许有用:当你在编写的解析器或编译器涉及到递归或者回退处理时,你需要扫描先前的部分,你可以clone并使用复制品,或者你在实现某种预编译处理,可以 clone 一些 lexer 来处理不同的输入文件。



创建克隆跟重新调用 lex.lex() 的不同点在于,PLY 不会重新构建任何的内部分析表或者正则式。当 lexer 是用类或者闭包创建的,需要注意类或闭包本身的的状态。换句话说你要注意新创建的 lexer 会共享原始 lexer 的这些状态,比如:



m = MyLexer()
a = lex.lex(object=m) # Create a lexer



b = a.clone() # Clone the lexer
Lexer 的内部状态
lexer 有一些内部属性在特定情况下有用:



lexer.lexpos。这是一个表示当前分析点的位置的整型值。如果你修改这个值的话,这会改变下一个 token() 的调用行为。在标记的规则方法里面,这个值表示紧跟匹配字串后面的第一个字符的位置,如果这个值在规则中修改,下一个返回的标记将从新的位置开始匹配
lexer.lineno。表示当前行号。PLY 只是声明这个属性的存在,却永远不更新这个值。如果你想要跟踪行号的话,你需要自己添加代码( 4.6 行号和位置信息)
lexer.lexdata。当前 lexer 的输入字串,这个字符串就是 input() 方法的输入字串,更改它可能是个糟糕的做法,除非你知道自己在干什么。
lexer.lexmatch。PLY 内部调用 Python 的 re.match() 方法得到的当前标记的原始的 Match 对象,该对象被保存在这个属性中。如果你的正则式中包含分组的话,你可以通过这个对象获得这些分组的值。注意:这个属性只在有标记规则定义的方法中才有效。
基于条件的扫描和启动条件
在高级的分析器应用程序中,使用状态化的词法扫描是很有用的。比如,你想在出现特定标记或句子结构的时候触发开始一个不同的词法分析逻辑。PLY 允许 lexer 在不同的状态之间转换。每个状态可以包含一些自己独特的标记和规则等。这是基于 GNU flex 的“启动条件”来实现的,关于 flex 详见 http://flex.sourceforge.net/manual/Start-Conditions.html#Start-Conditions



要使用 lex 的状态,你必须首先声明。通过在 lex 模块中声明”states”来做到:



states = (
(‘foo’,’exclusive’),
(‘bar’,’inclusive’),
)
这个声明中包含有两个状态:’foo’和’bar’。状态可以有两种类型:’排他型’和’包容型’。排他型的状态会使得 lexer 的行为发生完全的改变:只有能够匹配在这个状态下定义的规则的标记才会返回;包容型状态会将定义在这个状态下的规则添加到默认的规则集中,进而,只要能匹配这个规则集的标记都会返回。



一旦声明好之后,标记规则的命名需要包含状态名:



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
一个标记可以用在多个状态中,只要将多个状态名包含在声明中:



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_NUMBER = r’\d+’
t_INITIAL_NUMBER = r’\d+’
特殊的t_ignore()和t_error()也可以用状态关联:



t_foo_ignore = “ \t\n” # Ignored characters for state ‘foo’



def t_bar_error(t): # Special error handler for state ‘bar’
pass
词法分析默认在INITIAL状态下工作,这个状态下包含了所有默认的标记规则定义。对于不希望使用“状态”的用户来说,这是完全透明的。在分析过程中,如果你想要改变词法分析器的这种的状态,使用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
当你在面临很多状态可以选择进入,而又仅仅想要回到之前的状态时,状态栈比较有用。



举个例子会更清晰。假设你在写一个分析器想要从一堆 C 代码中获取任意匹配的闭合的大括号里面的部分:这意味着,当遇到起始括号’{‘,你需要读取与之匹配的’}’以上的所有部分。并返回字符串。使用通常的正则表达式几乎不可能,这是因为大括号可以嵌套,而且可以有注释,字符串等干扰。因此,试图简单的匹配第一个出现的’}’是不行的。这里你可以用lex的状态来做到:



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_rbrace 规则收集其中所有的代码(利用先前记录的开始位置),并保存,返回的标记类型为’CCODE’,与此同时,词法分析的状态退回到初始状态。



其他问题
lexer 需要输入的是一个字符串。好在大多数机器都有足够的内存,这很少导致性能的问题。这意味着,lexer 现在还不能用来处理文件流或者 socket 流。这主要是受到 re 模块的限制。
lexer 支持用 Unicode 字符描述标记的匹配规则,也支持输入字串包含 Unicode
如果你想要向re.compile()方法提供 flag,使用 reflags 选项:lex.lex(reflags=re.UNICODE)
由于 lexer 是全部用 Python 写的,性能很大程度上取决于 Python 的 re 模块,即使已经尽可能的高效了。当接收极其大量的输入文件时表现并不尽人意。如果担忧性能,你可以升级到最新的 Python,或者手工创建分析器,或者用 C 语言写 lexer 并做成扩展模块。
如果你要创建一个手写的词法分析器并计划用在 yacc.py 中,只需要满足下面的要求:



需要提供一个 token() 方法来返回下一个标记,如果没有可用的标记了,则返回 None。
token() 方法必须返回一个 tok 对象,具有 type 和 valu e属性。如果行号需要跟踪的话,标记还需要定义 lineno 属性。



一种理解语法指导翻译的好方法是将符号看成对象。与符号相关的值代表了符号的“状态”(比如上面的 val 属性),语义行为用一组操作符号及符号值的函数或者方法来表达。



Yacc 用的分析技术是著名的 LR 分析法或者叫移进-归约分析法。LR 分析法是一种自下而上的技术:首先尝试识别右部的语法规则,每当右部得到满足,相应的行为代码将被触发执行,当前右边的语法符号将被替换为左边的语法符号。(归约)



LR 分析法一般这样实现:将下一个符号进栈,然后结合栈顶的符号和后继符号(译者注:下一个将要输入符号),与文法中的某种规则相比较。具体的算法可以在编译器的手册中查到



由于分析表的得出相对开销较大(尤其包含大量的语法的情况下),分析表被写入当前目录的一个叫 parsetab.py 的文件中。除此之外,会生成一个调试文件 parser.out。在接下来的执行中,yacc 直到发现文法发生变化,才会重新生成分析表和 parsetab.py 文件,否则 yacc 会从 parsetab.py 中加载分析表。注:如果有必要的话这里输出的文件名是可以改的。



如果在你的文法中有任何错误的话,yacc.py 会产生调试信息,而且可能抛出异常。一些可以被检测到的错误如下:



方法重复定义(在语法文件中具有相同名字的方法)
二义文法产生的移进-归约和归约-归约冲突
指定了错误的文法
不可终止的递归(规则永远无法终结)
未使用的规则或标记
未定义的规则或标记
下面几个部分将更详细的讨论语法规则



这个例子的最后部分展示了如何执行由 yacc() 方法创建的分析器。你只需要简单的调用 parse(),并将输入字符串作为参数就能运行分析器。它将运行所有的语法规则,并返回整个分析的结果,这个结果就是在起始规则中赋给 p[0] 的值。



parser.out调试文件
使用 LR 分析算法跟踪移进/归约冲突和归约/归约冲突是件乐在其中的事。为了辅助调试,yacc.py 在生成分析表时会创建出一个调试文件叫 parser.out:



在p_error()方法中,有三个可用的方法来控制分析器的行为:



yacc.errok() 这个方法将分析器从恢复模式切换回正常模式。这会使得不会产生 error 标记,并重置内部的 error 计数器,而且下一个语法错误会再次产生 p_error() 调用
yacc.token() 这个方法用于得到下一个标记
yacc.restart() 这个方法抛弃当前整个分析栈,并重置分析器为起始状态
注意:这三个方法只能在p_error()中使用,不能用在其他任何地方。



p_error()方法也可以返回标记,这样能够控制将哪个标记作为下一个标记返回给分析器。这对于需要同步一些特殊标记的时候有用



在高级的分析器程序中,你可能同时需要多个语法和词法分析器。



依照规则行事不会有问题。不过,你需要小心确定所有东西都正确的绑定(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 属性指代的是当前触发规则的词法器对象:



代码执行了两个步骤:一个是对输入进行标记,这意味着它查找构成算术表达式的符号,第二步是解析,其中包括分析提取的标记并评估结果。



本节提供了一个如何标记用户输入,然后逐行分解的简单示例。



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()

# Give the lexer some input
lexer.input(data)

# Tokenize
while True:
tok = lexer.token()
if not tok:
break # No more input
print(tok) 将此文件另存为 calclex.py。我们将在构建 Yacc 解析器时使用它。


分解
使用 import ply.lex 导入模块



所有词法分析器必须提供名为 tokens 的列表,该列表定义词法分析器可以生成的所有可能的令牌名称。始终需要此列表。



tokens = [
‘NUMBER’,
‘PLUS’,
‘MINUS’,
‘TIMES’,
‘DIVIDE’,
‘LPAREN’,
‘RPAREN’,
]
tokens 也可以是字符串元组(而不是字符串),其中每个字符串表示如前所述的标记。



每个字符串的正则表达式规则可以定义为字符串或函数。在任何一种情况下,变量名都应以 t_作为前缀,以表示它是匹配令牌的规则。



对于简单的标记,正则表达式可以指定为字符串:t_PLUS = r’+’



如果需要执行某种操作,则可以将令牌规则指定为函数。



def t_NUMBER(t):
r’\d+’
t.value = int(t.value)
return t
请注意,规则在函数中指定为 doc 字符串。该函数接受一个参数,它是 LexToken 的一个实例,执行一些操作然后返回参数。



如果要使用外部字符串作为函数的正则表达式规则而不是指定文档字符串,请考虑以下示例:



@TOKEN(identifier) # identifier is a string holding the regex
def t_ID(t):
… # actions
LexToken 对象的一个​​实例(让我们称这个对象为 t)具有以下属性:



t.type 是令牌类型(作为字符串)(例如:’NUMBER’,’PLUS’等)。默认情况下,t.type 设置为 t_ 前缀后面的名称。
t.value 是 lexeme(实际文本匹配)
t.lineno 是当前行号(由于词法分析器不知道行号,因此不会自动更新)。使用名为 t_newline 的函数更新 lineno。
def t_newline(t):
r’\n+’
t.lexer.lineno += len(t.value)
t.lexpos,它是令牌相对于输入文本开头的位置。
如果从正则表达式规则函数返回任何内容,则丢弃该令牌。如果要丢弃令牌,可以选择将 t_ignore_前缀添加到正则表达式规则变量,而不是为同一规则定义函数。



def t_COMMENT(t):
r’#.*’
pass
# No return value. Token discarded
…是相同的:



t_ignore_COMMENT = r’#.*’
如果你在看到评论时执行某些操作,这当然是无效的。在这种情况下,使用函数来定义正则表达式规则。



如果你尚未为某些字符定义标记但仍想忽略它,请使用 t_ignore = “"(这些前缀是必需的):



t_ignore_COMMENT = r’#.*’
t_ignore = ‘ \t’ # ignores spaces and tabs
构建主正则表达式时,lex 将添加文件中指定的正则表达式,如下所示:



由函数定义的标记的添加顺序与它们在文件中出现的顺序相同。
由字符串定义的标记按照定义该标记的正则表达式的字符串的字符串长度的降序添加。
如果你在同一文件中匹配 == 和 =,请利用这些规则。



文字是按原样返回的标记。t.type 和 t.value 都将设置为角色本身。定义文字列表:



literals = [ ‘+’, ‘-‘, ‘*’, ‘/’ ]
要么,



literals = “+-*/”
在匹配文字时,可以编写执行附加操作的令牌函数。但是,你需要适当地设置令牌类型。例如:



literals = [ ‘{‘, ‘}’ ]



def t_lbrace(t):
r’{’
t.type = ‘{‘ # Set token type to the expected literal (ABSOLUTE MUST if this is a literal)
return t
使用 t_error 函数处理错误。



Error handling rule


def t_error(t):
print(“Illegal character ‘%s’” % t.value[0])
t.lexer.skip(1) # skip the illegal token (don’t process it)
通常,t.lexer.skip(n) 会跳过输入字符串中的 n 个字符。



最后准备:



使用 lexer = lex.lex() 构建词法分析器。



你还可以将所有内容放在类中,并调用该类的实例来定义词法分析器。例如:



import ply.lex as lex

class MyLexer(object):

… # everything relating to token rules and error handling comes here as usual



   # Build the lexer
def build(self, **kwargs):
self.lexer = lex.lex(module=self, **kwargs)

def test(self, data):
self.lexer.input(data)
for token in self.lexer.token():
print(token)

# Build the lexer and try it out


m = MyLexer()
m.build() # Build the lexer
m.test(“3 + 4”) #
使用 lexer.input(data) 提供输入,其中 data 是一个字符串



要获得令牌,请使用 lexer.token() 返回匹配的令牌。你可以在循环中迭代词法分析器,如下所示:



for i in lexer:
print(i)



本节介绍如何处理第 1 部分中的标记化输入 - 使用 Context Free Grammars(CFG) 完成。必须指定语法,并根据语法处理标记。在引擎盖下,解析器使用 LALR 解析器。



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)
分解
每个语法规则由一个函数定义,其中该函数的文档字符串包含适当的无上下文语法规范。构成函数体的语句实现规则的语义动作。每个函数接受单个参数 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] 对于标记,相应 p i 的值与词法模块中分配的 p.value 属性相同。所以,PLUS 将具有+的价值。


对于非终端,该值由 p
0
中的任何内容确定。如果未放置任何内容,则值为 None。此外,p
−1
与 p
3
不同,因为 p 不是一个简单的列表(p
−1
可以指定嵌入式动作(这里不讨论))。



请注意,该函数可以具有任何名称,只要它在 p_ 之前。



定义 p_error(p) 规则以捕获语法错误(与 yacc / bison 中的 yyerror 相同)。



多个语法规则可以组合成一个单独的函数,如果制作具有类似的结构,这是一个好主意。



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]
可以使用字符文字代替标记。



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]
当然,必须在词法分析器模块中指定文字。



空作品的形式为’'’symbol : ‘’’



要明确设置起始符号,请使用 start = ‘foo’,其中 foo 是非终端符号。



可以使用优先级变量来设置优先级和关联性。



precedence = (
(‘nonassoc’, ‘LESSTHAN’, ‘GREATERTHAN’), # Nonassociative operators
(‘left’, ‘PLUS’, ‘MINUS’),
(‘left’, ‘TIMES’, ‘DIVIDE’),
(‘right’, ‘UMINUS’), # Unary minus operator
)
令牌从最低到最高排序。nonassoc 意味着那些令牌不相关。这意味着像 a < b < c 这样的东西是非法的,而 a < b 仍然是合法的。



parser.out 是第一次执行 yacc 程序时创建的调试文件。每当发生移位/减少冲突时,解析器总是移位。



Category python