本文是PLY (Python Lex-Yacc)的中文翻譯版。轉載請注明出處。這里有更好的閱讀體驗。
如果你從事編譯器或解析器的開發工作,你可能對lex和yacc不會陌生,PLY是David Beazley實現的基於Python的lex和yacc。作者最著名的成就可能是其撰寫的Python Cookbook, 3rd Edition。我因為偶然的原因接觸了PLY,覺得是個好東西,但是似乎國內沒有相關的資料。於是萌生了翻譯的想法,雖然內容不算多,但是由於能力有限,很多概念不了解,還專門補習了編譯原理,這對我有很大幫助。為了完成翻譯,經過初譯,復審,排版等,花費我很多時間,最終還是堅持下來了,希望對需要的人有所幫助。另外,第一次大規模翻譯英文,由於水平有限,如果錯誤或者不妥的地方還請指正,非常感謝。
目錄
1 前言和預備
本文指導你使用PLY進行詞法分析和語法解析的,鑒於解析本身是個復雜性的事情,在你使用PLY投入大規模的開發前,我強烈建議你完整地閱讀或者瀏覽本文檔。
PLY-3.0能同時兼容Python2和Python3。需要注意的是,對於Python3的支持是新加入的,還沒有廣泛的測試(盡管所有的例子和單元測試都能夠在Pythone3下通過)。如果你使用的是Python2,應該使用Python2.4以上版本,雖然,PLY最低能夠支持到Python2.2,不過一些可選的功能需要新版本模塊的支持。
2 介紹
PLY是純粹由Python實現的Lex和yacc(流行的編譯器構建工具)。PLY的設計目標是盡可能的沿襲傳統lex和yacc工具的工作方式,包括支持LALR(1)分析法、提供豐富的輸入驗證、錯誤報告和診斷。因此,如果你曾經在其他編程語言下使用過yacc,你應該能夠很容易的遷移到PLY上。
2001年,我在芝加哥大學教授“編譯器簡介”課程時開發了的早期的PLY。學生們使用Python和PLY構建了一個類似Pascal的語言的完整編譯器,其中的語言特性包括:詞法分析、語法分析、類型檢查、類型推斷、嵌套作用域,並針對SPARC處理器生成目標代碼等。最終他們大約實現了30種不同的編譯器!PLY在接口設計上影響使用的問題也被學生們所提出。從2001年以來,PLY繼續從用戶的反饋中不斷改進。為了適應對未來的改進需求,PLY3.0在原來基礎上進行了重大的重構。
由於PLY是作為教學工具來開發的,你會發現它對於標記和語法規則是相當嚴謹的,這一定程度上是為了幫助新手用戶找出常見的編程錯誤。不過,高級用戶也會發現這有助於處理真實編程語言的復雜語法。還需要注意的是,PLY沒有提供太多花哨的東西(例如,自動構建抽象語法樹和遍歷樹),我也不認為它是個分析框架。相反,你會發現它是一個用Python實現的,基本的,但能夠完全勝任的lex/yacc。
本文的假設你多少熟悉分析理論、語法制導的翻譯、基於其他編程語言使用過類似lex和yacc的編譯器構建工具。如果你對這些東西不熟悉,你可能需要先去一些書籍中學習一些基礎,比如:Aho, Sethi和Ullman的《Compilers: Principles, Techniques, and Tools》(《編譯原理》),和O'Reilly'出版的John Levine的《lex and yacc》。事實上,《lex and yacc》和PLY使用的概念幾乎相同。
3 PLY概要
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會緩存生成的分析表,並將它們保存在獨立的文件中,除非源文件有變化,會重新生成分析表,否則將從緩存中直接讀取。
4 Lex
lex.py是用來將輸入字符串標記化。例如,假設你正在設計一個編程語言,用戶的輸入字符串如下:
x = 3 + 42 * (s - t)
標記器將字符串分割成獨立的標記:
'x','=', '3', '+', '42', '*', '(', 's', '-', 't', ')'
標記通常用一組名字來命名和表示:
'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',')
正則表達式是描述標記規則的典型方法,下一節展示如何用lex.py實現。
4.1 Lex的例子
下面的例子展示了如何使用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()
為了使lexer工作,你需要給定一個輸入,並傳遞給input()方法。然后,重復調用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
程序執行,將給出如下輸出:
$ python example.py
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)
Lexers也同時支持迭代,你可以把上面的循環寫成這樣:
for tok in lexer:
print tok
由lexer.token()
方法返回的標記是LexToken
類型的實例,擁有tok.type
,tok.value
,tok.lineno
和tok.lexpos
屬性,下面的代碼展示了如何訪問這些屬性:
# Tokenize
while True:
tok = lexer.token()
if not tok: break # No more input
print tok.type, tok.value, tok.line, tok.lexpos
tok.type
和tok.value
屬性表示標記本身的類型和值。tok.line
和tok.lexpos
屬性包含了標記的位置信息,tok.lexpos
表示標記相對於輸入串起始位置的偏移。
4.2 標記列表
詞法分析器必須提供一個標記的列表,這個列表將所有可能的標記告訴分析器,用來執行各種驗證,同時也提供給yacc.py作為終結符。
在上面的例子中,是這樣給定標記列表的:
tokens = (
'NUMBER',
'PLUS',
'MINUS',
'TIMES',
'DIVIDE',
'LPAREN',
'RPAREN',
)
4.3 標記的規則
每種標記用一個正則表達式規則來表示,每個規則需要以"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',這通常不是你想要的。
4.4 標記的值
標記被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。
4.5 丟棄標記
想丟棄像注釋之類的標記,只要不返回value就行了,像這樣:
def t_COMMENT(t):
r'\#.*'
pass
# No return value. Token discarded
為標記聲明添加"ignore_"前綴同樣可以達到目的:
t_ignore_COMMENT = r'\#.*'
如果有多種文本需要丟棄,建議使用方法來定義規則,因為方法能夠提供更精確的匹配優先級控制(方法根據出現的順序,而字符串的正則表達式依據正則表達式的長度)
4.6 行號和位置信息
默認情況下,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
通常,計算列的信息是為了指示上下文的錯誤位置,所以只在必要時有用。
4.7 忽略字符
t_ignore
規則比較特殊,是lex.py所保留用來忽略字符的,通常用來跳過空白或者不需要的字符。雖然可以通過定義像t_newline()
這樣的規則來完成相同的事情,不過使用t_ignore能夠提供較好的詞法分析性能,因為相比普通的正則式,它被特殊化處理了。
4.8 字面字符
字面字符可以通過在詞法模塊中定義一個literals
變量做到,例如:
literals = [ '+','-','*','/' ]
或者
literals = "+-*/"
字面字符是指單個字符,表示把字符本身作為標記,標記的type
和value
都是字符本身。不過,字面字符是在其他正則式之后被檢查的,因此如果有規則是以這些字符開頭的,那么這些規則的優先級較高。
4.9 錯誤處理
最后,在詞法分析中遇到非法字符時,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)
跳過一個字符。
4.10 構建和使用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對象的對應方法上。最好不要這樣用,因為這種接口可能不知道在什么時候就失效(譯者注:垃圾回收?)
4.11 @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的兼容性問題,使用上面的等價辦法。
4.12 優化模式
為了提高性能,你可能希望使用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會被禁用大多數的錯誤檢查。因此,建議只在確保萬事俱備准備發布最終代碼時使用。
4.13 調試
如果想要調試,可以使lex()運行在調試模式:
lexer = lex.lex(debug=1)
這將打出一些調試信息,包括添加的規則、最終的正則表達式和詞法分析過程中得到的標記。
除此之外,lex.py有一個簡單的主函數,不但支持對命令行參數輸入的字串進行掃描,還支持命令行參數指定的文件名:
if __name__ == '__main__':
lex.runmain()
想要了解高級調試的詳情,請移步至最后的高級調試部分。
4.14 其他方式定義詞法規則
上面的例子,詞法分析器都是在單個的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()
4.15 額外狀態維護
在你的詞法分析器中,你可能想要維護一些狀態。這可能包括模式設置,符號表和其他細節。例如,假設你想要跟蹤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
...
4.16 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
4.17 Lexer的內部狀態
lexer有一些內部屬性在特定情況下有用:
lexer.lexpos
。這是一個表示當前分析點的位置的整型值。如果你修改這個值的話,這會改變下一個token()
的調用行為。在標記的規則方法里面,這個值表示緊跟匹配字串后面的第一個字符的位置,如果這個值在規則中修改,下一個返回的標記將從新的位置開始匹配lexer.lineno
。表示當前行號。PLY只是聲明這個屬性的存在,卻永遠不更新這個值。如果你想要跟蹤行號的話,你需要自己添加代碼( 4.6 行號和位置信息)lexer.lexdata
。當前lexer的輸入字串,這個字符串就是input()方法的輸入字串,更改它可能是個糟糕的做法,除非你知道自己在干什么。lexer.lexmatch
。PLY內部調用Python的re.match()方法得到的當前標記的原始的Match對象,該對象被保存在這個屬性中。如果你的正則式中包含分組的話,你可以通過這個對象獲得這些分組的值。注意:這個屬性只在有標記規則定義的方法中才有效。
4.18 基於條件的掃描和啟動條件
在高級的分析器應用程序中,使用狀態化的詞法掃描是很有用的。比如,你想在出現特定標記或句子結構的時候觸發開始一個不同的詞法分析邏輯。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',與此同時,詞法分析的狀態退回到初始狀態。
4.19 其他問題
- 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
和value
屬性。如果行號需要跟蹤的話,標記還需要定義lineno
屬性。
5 語法分析基礎
yacc.py用來對語言進行語法分析。在給出例子之前,必須提一些重要的背景知識。首先,‘語法’通常用BNF范式來表達。例如,如果想要分析簡單的算術表達式,你應該首先寫下無二義的文法:
expression : expression + term
| expression - term
| term
term : term * factor
| term / factor
| factor
factor : NUMBER
| ( expression )
在這個文法中,像NUMBER
,+
,-
,*
,/
的符號被稱為終結符,對應原始的輸入。類似term
,factor
等稱為非終結符,它們由一系列終結符或其他規則的符號組成,用來指代語法規則。
通常使用一種叫語法制導翻譯的技術來指定某種語言的語義。在語法制導翻譯中,符號及其屬性出現在每個語法規則后面的動作中。每當一個語法被識別,動作就能夠描述需要做什么。比如,對於上面給定的文法,想要實現一個簡單的計算器,應該寫成下面這樣:
Grammar Action
-------------------------------- --------------------------------------------
expression0 : expression1 + term expression0.val = expression1.val + term.val
| expression1 - term expression0.val = expression1.val - term.val
| term expression0.val = term.val
term0 : term1 * factor term0.val = term1.val * factor.val
| term1 / factor term0.val = term1.val / factor.val
| factor term0.val = factor.val
factor : NUMBER factor.val = int(NUMBER.lexval)
| ( expression ) factor.val = expression.val
一種理解語法指導翻譯的好方法是將符號看成對象。與符號相關的值代表了符號的“狀態”(比如上面的val屬性),語義行為用一組操作符號及符號值的函數或者方法來表達。
Yacc用的分析技術是著名的LR分析法或者叫移進-歸約分析法。LR分析法是一種自下而上的技術:首先嘗試識別右部的語法規則,每當右部得到滿足,相應的行為代碼將被觸發執行,當前右邊的語法符號將被替換為左邊的語法符號。(歸約)
LR分析法一般這樣實現:將下一個符號進棧,然后結合棧頂的符號和后繼符號(譯者注:下一個將要輸入符號),與文法中的某種規則相比較。具體的算法可以在編譯器的手冊中查到,下面的例子展現了如果通過上面定義的文法,來分析3 + 5 * ( 10 - 20 )這個表達式,$用來表示輸入結束
Step Symbol Stack Input Tokens Action
---- --------------------- --------------------- -------------------------------
1 3 + 5 * ( 10 - 20 )$ Shift 3
2 3 + 5 * ( 10 - 20 )$ Reduce factor : NUMBER
3 factor + 5 * ( 10 - 20 )$ Reduce term : factor
4 term + 5 * ( 10 - 20 )$ Reduce expr : term
5 expr + 5 * ( 10 - 20 )$ Shift +
6 expr + 5 * ( 10 - 20 )$ Shift 5
7 expr + 5 * ( 10 - 20 )$ Reduce factor : NUMBER
8 expr + factor * ( 10 - 20 )$ Reduce term : factor
9 expr + term * ( 10 - 20 )$ Shift *
10 expr + term * ( 10 - 20 )$ Shift (
11 expr + term * ( 10 - 20 )$ Shift 10
12 expr + term * ( 10 - 20 )$ Reduce factor : NUMBER
13 expr + term * ( factor - 20 )$ Reduce term : factor
14 expr + term * ( term - 20 )$ Reduce expr : term
15 expr + term * ( expr - 20 )$ Shift -
16 expr + term * ( expr - 20 )$ Shift 20
17 expr + term * ( expr - 20 )$ Reduce factor : NUMBER
18 expr + term * ( expr - factor )$ Reduce term : factor
19 expr + term * ( expr - term )$ Reduce expr : expr - term
20 expr + term * ( expr )$ Shift )
21 expr + term * ( expr ) $ Reduce factor : (expr)
22 expr + term * factor $ Reduce term : term * factor
23 expr + term $ Reduce expr : expr + term
24 expr $ Reduce expr
25 $ Success!
(譯者注:action里面的Shift就是進棧動作,簡稱移進;Reduce是歸約)
在分析表達式的過程中,一個相關的自動狀態機和后繼符號決定了下一步應該做什么。如果下一個標記看起來是一個有效語法(產生式)的一部分(通過棧上的其他項判斷這一點),那么這個標記應該進棧。如果棧頂的項可以組成一個完整的右部語法規則,一般就可以進行“歸約”,用產生式左邊的符號代替這一組符號。當歸約發生時,相應的行為動作就會執行。如果輸入標記既不能移進也不能歸約的話,就會發生語法錯誤,分析器必須進行相應的錯誤恢復。分析器直到棧空並且沒有另外的輸入標記時,才算成功。
需要注意的是,這是基於一個有限自動機實現的,有限自動器被轉化成分析表。分析表的構建比較復雜,超出了本文的討論范圍。不過,這構建過程的微妙細節能夠解釋為什么在上面的例子中,解析器選擇在步驟9
將標記轉移到堆棧中,而不是按照規則expr : expr + term做歸約。
6 Yacc
ply.yacc模塊實現了PLY的分析功能,‘yacc’是‘Yet Another Compiler Compiler’的縮寫並保留了其作為Unix工具的名字。
6.1 一個例子
假設你希望實現上面的簡單算術表達式的語法分析,代碼如下:
# 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]
其中,p[i]
的值相當於詞法分析模塊中對p.value
屬性賦的值,對於非終結符的值,將在歸約時由p[0]
的賦值決定,這里的值可以是任何類型,當然,大多數情況下只是Python的簡單類型、元組或者類的實例。在這個例子中,我們依賴這樣一個事實:NUMBER
標記的值保存的是整型值,所有規則的行為都是得到這些整型值的算術運算結果,並傳遞結果。
注意:在這里負數的下標有特殊意義--這里的p[-1]不等同於p[3]。詳見下面的嵌入式動作部分
在yacc中定義的第一個語法規則被默認為起始規則(這個例子中的第一個出現的expression規則)。一旦起始規則被分析器歸約,而且再無其他輸入,分析器終止,最后的值將返回(這個值將是起始規則的p[0])。注意:也可以通過在yacc()
中使用start關鍵字參數來指定起始規則
p_error
(p)規則用於捕獲語法錯誤。詳見處理語法錯誤部分
為了構建分析器,需要調用yacc.yacc()
方法。這個方法查看整個當前模塊,然后試圖根據你提供的文法構建LR分析表。第一次執行yacc.yacc()
,你會得到如下輸出:
$ python calcparse.py
Generating LALR tables
calc >
由於分析表的得出相對開銷較大(尤其包含大量的語法的情況下),分析表被寫入當前目錄的一個叫parsetab.py
的文件中。除此之外,會生成一個調試文件parser.out
。在接下來的執行中,yacc直到發現文法發生變化,才會重新生成分析表和parsetab.py文件,否則yacc會從parsetab.py中加載分析表。注:如果有必要的話這里輸出的文件名是可以改的。
如果在你的文法中有任何錯誤的話,yacc.py會產生調試信息,而且可能拋出異常。一些可以被檢測到的錯誤如下:
- 方法重復定義(在語法文件中具有相同名字的方法)
- 二義文法產生的移進-歸約和歸約-歸約沖突
- 指定了錯誤的文法
- 不可終止的遞歸(規則永遠無法終結)
- 未使用的規則或標記
- 未定義的規則或標記
下面幾個部分將更詳細的討論語法規則
這個例子的最后部分展示了如何執行由yacc()
方法創建的分析器。你只需要簡單的調用parse()
,並將輸入字符串作為參數就能運行分析器。它將運行所有的語法規則,並返回整個分析的結果,這個結果就是在起始規則中賦給p[0]
的值。
6.2 將語法規則合並
如果語法規則類似的話,可以合並到一個方法中。例如,考慮前面例子中的兩個規則:
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]
如果考慮解析的性能,你應該避免像這些例子一樣在一個語法規則里面用很多條件來處理。因為,每次檢查當前究竟匹配的是哪個語法規則的時候,實際上重復做了分析器已經做過的事(分析器已經准確的知道哪個規則被匹配了)。為每個規則定義單獨的方法,可以消除這點開銷。
6.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]
字符必須像'+'那樣使用單引號。除此之外,需要將用到的字符定義單獨定義在lex文件的literals
列表里:
# Literals. Should be placed in module given to lex()
literals = ['+','-','*','/' ]
字面的字符只能是單個字符。因此,像'<='或者'=='都是不合法的,只能使用一般的詞法規則(例如t_EQ = r'==')。
6.4 空產生式
yacc.py可以處理空產生式,像下面這樣做:
def p_empty(p):
'empty :'
pass
現在可以使用空匹配,只要將'empty
'當成一個符號使用:
def p_optitem(p):
'optitem : item'
' | empty'
...
注意:你可以將產生式保持'空',來表示空匹配。然而,我發現用一個'empty
'規則並用其來替代'空',更容易表達意圖,並有較好的可讀性。
6.5 改變起始符號
默認情況下,在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
標識有助於在調試的時候將大型的語法規則分成小部分來分析。也可把start符號作為yacc的參數:
yacc.yacc(start='foo')
6.6 處理二義文法
上面例子中,對表達式的文法描述用一種特別的形式規避了二義文法。然而,在很多情況下,這樣的特殊文法很難寫,或者很別扭。一個更為自然和舒服的語法表達應該是這樣的:
expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression
| LPAREN expression RPAREN
| NUMBER
不幸的是,這樣的文法是存在二義性的。舉個例子,如果你要解析字符串"3 * 4 + 5",操作符如何分組並沒有指明,究竟是表示"(3 * 4) + 5"還是"3 * (4 + 5)"呢?
如果在yacc.py中存在二義文法,會輸出"移進歸約沖突"或者"歸約歸約沖突"。在分析器無法確定是將下一個符號移進棧還是將當前棧中的符號歸約時會產生移進歸約沖突。例如,對於"3 * 4 + 5",分析器內部棧是這樣工作的:
Step Symbol Stack Input Tokens Action
---- --------------------- --------------------- -------------------------------
1 $ 3 * 4 + 5$ Shift 3
2 $ 3 * 4 + 5$ Reduce : expression : NUMBER
3 $ expr * 4 + 5$ Shift *
4 $ expr * 4 + 5$ Shift 4
5 $ expr * 4 + 5$ Reduce: expression : NUMBER
6 $ expr * expr + 5$ SHIFT/REDUCE CONFLICT ????
在這個例子中,當分析器來到第6步的時候,有兩種選擇:一是按照expr : expr * expr歸約,一是將標記'+'繼續移進棧。兩種選擇對於上面的上下文無關文法而言都是合法的。
默認情況下,所有的移進歸約沖突會傾向於使用移進來處理。因此,對於上面的例子,分析器總是會將'+'進棧,而不是做歸約。雖然在很多情況下,這個策略是合適的(像"if-then"和"if-then-else"),但這對於算術表達式是不夠的。事實上,對於上面的例子,將'+'進棧是完全錯誤的,應當先將expr * expr歸約,因為乘法的優先級要高於加法。
為了解決二義文法,尤其是對表達式文法,yacc.py允許為標記單獨指定優先級和結合性。需要像下面這樣增加一個precedence變量:
precedence = (
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE'),
)
這樣的定義說明PLUS/MINUS
標記具有相同的優先級和左結合性,TIMES/DIVIDE
具有相同的優先級和左結合性。在precedence聲明中,標記的優先級從低到高。因此,這個聲明表明TIMES/DIVIDE
(他們較晚加入precedence
)的優先級高於PLUS/MINUS
。
由於為標記添加了數字表示的優先級和結合性的屬性,所以,對於上面的例子,將會得到:
PLUS : level = 1, assoc = 'left'
MINUS : level = 1, assoc = 'left'
TIMES : level = 2, assoc = 'left'
DIVIDE : level = 2, assoc = 'left'
隨后這些值被附加到語法規則的優先級和結合性屬性上,這些值由最右邊的終結符的優先級和結合性決定:
expression : expression PLUS expression # level = 1, left
| expression MINUS expression # level = 1, left
| expression TIMES expression # level = 2, left
| expression DIVIDE expression # level = 2, left
| LPAREN expression RPAREN # level = None (not specified)
| NUMBER # level = None (not specified)
當出現移進歸約沖突時,分析器生成器根據下面的規則解決二義文法:
- 如果當前的標記的優先級高於棧頂規則的優先級,移進當前標記
- 如果棧頂規則的優先級更高,進行歸約
- 如果當前的標記與棧頂規則的優先級相同,如果標記是左結合的,則歸約,否則,如果是右結合的則移進
- 如果沒有優先級可以參考,默認對於移進歸約沖突執行移進
比如,當解析到"expression PLUS expression"這個語法時,下一個標記是TIMES
,此時將執行移進,因為TIMES
具有比PLUS
更高的優先級;當解析到"expression TIMES expression",下一個標記是PLUS
,此時將執行歸約,因為PLUS
的優先級低於TIMES
。
如果在使用前三種技術解決已經歸約沖突后,yacc.py將不會報告語法中的沖突或者錯誤(不過,會在parser.out這個調試文件中輸出一些信息)
使用precedence
指定優先級的技術會帶來一個問題,有時運算符的優先級需要基於上下文。例如,考慮"3 + 4 * -5"中的一元的'-'。數學上講,一元運算符應當擁有較高的優先級。然而,在我們的precedence
定義中,MINUS
的優先級卻低於TIMES
。為了解決這個問題,precedene
規則中可以包含"虛擬標記":
precedence = (
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE'),
('right', 'UMINUS'), # Unary minus operator
)
在語法文件中,我們可以這么表示一元算符:
def p_expr_uminus(p):
'expression : MINUS expression %prec UMINUS'
p[0] = -p[2]
在這個例子中,%prec UMINUS
覆蓋了默認的優先級(MINUS
的優先級),將UMINUS
指代的優先級應用在該語法規則上。
起初,UMINUS
標記的例子會讓人感到困惑。UMINUS
既不是輸入的標記也不是語法規則,你應當將其看成precedence
表中的特殊的占位符。當你使用%prec
宏時,你是在告訴yacc,你希望表達式使用這個占位符所表示的優先級,而不是正常的優先級。
還可以在precedence
表中指定"非關聯"。這表明你不希望鏈式運算符。比如,假如你希望支持比較運算符'<'和'>',但是你不希望支持 a < b < c,只要簡單指定規則如下:
precedence = (
('nonassoc', 'LESSTHAN', 'GREATERTHAN'), # Nonassociative operators
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE'),
('right', 'UMINUS'), # Unary minus operator
)
此時,當輸入形如 a < b < c時,將產生語法錯誤,卻不影響形如 a < b 的表達式。
對於給定的符號集,存在多種語法規則可以匹配時會產生歸約/歸約沖突。這樣的沖突往往很嚴重,而且總是通過匹配最早出現的語法規則來解決。歸約/歸約沖突幾乎總是相同的符號集合具有不同的規則可以匹配,而在這一點上無法抉擇,比如:
assignment : ID EQUALS NUMBER
| ID EQUALS expression
expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression
| LPAREN expression RPAREN
| NUMBER
這個例子中,對於下面這兩條規則將產生歸約/歸約沖突:
assignment : ID EQUALS NUMBER
expression : NUMBER
比如,對於"a = 5",分析器不知道應當按照assignment : ID EQUALS NUMBER歸約,還是先將5歸約成expression,再歸約成assignment : ID EQUALS expression。
應當指出的是,只是簡單的查看語法規則是很難減少歸約/歸約沖突。如果出現歸約/歸約沖突,yacc()會幫助打印出警告信息:
WARNING: 1 reduce/reduce conflict
WARNING: reduce/reduce conflict in state 15 resolved using rule (assignment -> ID EQUALS NUMBER)
WARNING: rejected rule (expression -> NUMBER)
上面的信息標識出了沖突的兩條規則,但是,並無法指出究竟在什么情況下會出現這樣的狀態。想要發現問題,你可能需要結合語法規則和parser.out調試文件的內容。
6.7 parser.out調試文件
使用LR分析算法跟蹤移進/歸約沖突和歸約/歸約沖突是件樂在其中的事。為了輔助調試,yacc.py在生成分析表時會創建出一個調試文件叫parser.out
:
Unused terminals:
Grammar
Rule 1 expression -> expression PLUS expression
Rule 2 expression -> expression MINUS expression
Rule 3 expression -> expression TIMES expression
Rule 4 expression -> expression DIVIDE expression
Rule 5 expression -> NUMBER
Rule 6 expression -> LPAREN expression RPAREN
Terminals, with rules where they appear
TIMES : 3
error :
MINUS : 2
RPAREN : 6
LPAREN : 6
DIVIDE : 4
PLUS : 1
NUMBER : 5
Nonterminals, with rules where they appear
expression : 1 1 2 2 3 3 4 4 6 0
Parsing method: LALR
state 0
S' -> . expression
expression -> . expression PLUS expression
expression -> . expression MINUS expression
expression -> . expression TIMES expression
expression -> . expression DIVIDE expression
expression -> . NUMBER
expression -> . LPAREN expression RPAREN
NUMBER shift and go to state 3
LPAREN shift and go to state 2
state 1
S' -> expression .
expression -> expression . PLUS expression
expression -> expression . MINUS expression
expression -> expression . TIMES expression
expression -> expression . DIVIDE expression
PLUS shift and go to state 6
MINUS shift and go to state 5
TIMES shift and go to state 4
DIVIDE shift and go to state 7
state 2
expression -> LPAREN . expression RPAREN
expression -> . expression PLUS expression
expression -> . expression MINUS expression
expression -> . expression TIMES expression
expression -> . expression DIVIDE expression
expression -> . NUMBER
expression -> . LPAREN expression RPAREN
NUMBER shift and go to state 3
LPAREN shift and go to state 2
state 3
expression -> NUMBER .
$ reduce using rule 5
PLUS reduce using rule 5
MINUS reduce using rule 5
TIMES reduce using rule 5
DIVIDE reduce using rule 5
RPAREN reduce using rule 5
state 4
expression -> expression TIMES . expression
expression -> . expression PLUS expression
expression -> . expression MINUS expression
expression -> . expression TIMES expression
expression -> . expression DIVIDE expression
expression -> . NUMBER
expression -> . LPAREN expression RPAREN
NUMBER shift and go to state 3
LPAREN shift and go to state 2
state 5
expression -> expression MINUS . expression
expression -> . expression PLUS expression
expression -> . expression MINUS expression
expression -> . expression TIMES expression
expression -> . expression DIVIDE expression
expression -> . NUMBER
expression -> . LPAREN expression RPAREN
NUMBER shift and go to state 3
LPAREN shift and go to state 2
state 6
expression -> expression PLUS . expression
expression -> . expression PLUS expression
expression -> . expression MINUS expression
expression -> . expression TIMES expression
expression -> . expression DIVIDE expression
expression -> . NUMBER
expression -> . LPAREN expression RPAREN
NUMBER shift and go to state 3
LPAREN shift and go to state 2
state 7
expression -> expression DIVIDE . expression
expression -> . expression PLUS expression
expression -> . expression MINUS expression
expression -> . expression TIMES expression
expression -> . expression DIVIDE expression
expression -> . NUMBER
expression -> . LPAREN expression RPAREN
NUMBER shift and go to state 3
LPAREN shift and go to state 2
state 8
expression -> LPAREN expression . RPAREN
expression -> expression . PLUS expression
expression -> expression . MINUS expression
expression -> expression . TIMES expression
expression -> expression . DIVIDE expression
RPAREN shift and go to state 13
PLUS shift and go to state 6
MINUS shift and go to state 5
TIMES shift and go to state 4
DIVIDE shift and go to state 7
state 9
expression -> expression TIMES expression .
expression -> expression . PLUS expression
expression -> expression . MINUS expression
expression -> expression . TIMES expression
expression -> expression . DIVIDE expression
$ reduce using rule 3
PLUS reduce using rule 3
MINUS reduce using rule 3
TIMES reduce using rule 3
DIVIDE reduce using rule 3
RPAREN reduce using rule 3
! PLUS [ shift and go to state 6 ]
! MINUS [ shift and go to state 5 ]
! TIMES [ shift and go to state 4 ]
! DIVIDE [ shift and go to state 7 ]
state 10
expression -> expression MINUS expression .
expression -> expression . PLUS expression
expression -> expression . MINUS expression
expression -> expression . TIMES expression
expression -> expression . DIVIDE expression
$ reduce using rule 2
PLUS reduce using rule 2
MINUS reduce using rule 2
RPAREN reduce using rule 2
TIMES shift and go to state 4
DIVIDE shift and go to state 7
! TIMES [ reduce using rule 2 ]
! DIVIDE [ reduce using rule 2 ]
! PLUS [ shift and go to state 6 ]
! MINUS [ shift and go to state 5 ]
state 11
expression -> expression PLUS expression .
expression -> expression . PLUS expression
expression -> expression . MINUS expression
expression -> expression . TIMES expression
expression -> expression . DIVIDE expression
$ reduce using rule 1
PLUS reduce using rule 1
MINUS reduce using rule 1
RPAREN reduce using rule 1
TIMES shift and go to state 4
DIVIDE shift and go to state 7
! TIMES [ reduce using rule 1 ]
! DIVIDE [ reduce using rule 1 ]
! PLUS [ shift and go to state 6 ]
! MINUS [ shift and go to state 5 ]
state 12
expression -> expression DIVIDE expression .
expression -> expression . PLUS expression
expression -> expression . MINUS expression
expression -> expression . TIMES expression
expression -> expression . DIVIDE expression
$ reduce using rule 4
PLUS reduce using rule 4
MINUS reduce using rule 4
TIMES reduce using rule 4
DIVIDE reduce using rule 4
RPAREN reduce using rule 4
! PLUS [ shift and go to state 6 ]
! MINUS [ shift and go to state 5 ]
! TIMES [ shift and go to state 4 ]
! DIVIDE [ shift and go to state 7 ]
state 13
expression -> LPAREN expression RPAREN .
$ reduce using rule 6
PLUS reduce using rule 6
MINUS reduce using rule 6
TIMES reduce using rule 6
DIVIDE reduce using rule 6
RPAREN reduce using rule 6
文件中出現的不同狀態,代表了有效輸入標記的所有可能的組合,這是依據文法規則得到的。當得到輸入標記時,分析器將構造一個棧,並找到匹配的規則。每個狀態跟蹤了當前輸入進行到語法規則中的哪個位置,在每個規則中,'.'表示當前分析到規則的哪個位置,而且,對於在當前狀態下,輸入的每個有效標記導致的動作也被羅列出來。當出現移進/歸約或歸約/歸約沖突時,被忽略的規則前面會添加!,就像這樣:
! TIMES [ reduce using rule 2 ]
! DIVIDE [ reduce using rule 2 ]
! PLUS [ shift and go to state 6 ]
! MINUS [ shift and go to state 5 ]
通過查看這些規則並結合一些實例,通常能夠找到大部分沖突的根源。應該強調的是,不是所有的移進歸約沖突都是不好的,想要確定解決方法是否正確,唯一的辦法就是查看parser.out。
6.8 處理語法錯誤
如果你創建的分析器用於產品,處理語法錯誤是很重要的。一般而言,你不希望分析器在遇到錯誤的時候就拋出異常並終止,相反,你需要它報告錯誤,盡可能的恢復並繼續分析,一次性的將輸入中所有的錯誤報告給用戶。這是一些已知語言編譯器的標准行為,例如C,C++,Java。在PLY中,在語法分析過程中出現錯誤,錯誤會被立即檢測到(分析器不會繼續讀取源文件中錯誤點后面的標記)。然而,這時,分析器會進入恢復模式,這個模式能夠用來嘗試繼續向下分析。LR分析器的錯誤恢復是個理論與技巧兼備的問題,yacc.py提供的錯誤機制與Unix下的yacc類似,所以你可以從諸如O'Reilly出版的《Lex and yacc》的書中找到更多的細節。
當錯誤發生時,yacc.py按照如下步驟進行:
- 第一次錯誤產生時,用戶定義的
p_error()
方法會被調用,出錯的標記會作為參數傳入;如果錯誤是因為到達文件結尾造成的,傳入的參數將為None
。隨后,分析器進入到“錯誤恢復”模式,該模式下不會在產生p_error()
調用,直到它成功的移進3個標記,然后回歸到正常模式。 - 如果在
p_error()
中沒有指定恢復動作的話,這個導致錯誤的標記會被替換成一個特殊的error
標記。 - 如果導致錯誤的標記已經是
error
的話,原先的棧頂的標記將被移除。 - 如果整個分析棧被放棄,分析器會進入重置狀態,並從他的初始狀態開始分析。
- 如果此時的語法規則接受
error
標記,error
標記會移進棧。 - 如果當前棧頂是
error
標記,之后的標記將被忽略,直到有標記能夠導致error
的歸約。
6.8.1 根據error規則恢復和再同步
最佳的處理語法錯誤的做法是在語法規則中包含error
標記。例如,假設你的語言有一個關於print的語句的語法規則:
def p_statement_print(p):
'statement : PRINT expr SEMI'
...
為了處理可能的錯誤表達式,你可以添加一條額外的語法規則:
def p_statement_print_error(p):
'statement : PRINT error SEMI'
print "Syntax error in print statement. Bad expression"
這樣(expr錯誤時),error
標記會匹配任意多個分號之前的標記(分號是SEMI
指代的字符)。一旦找到分號,規則將被匹配,這樣error
標記就被歸約了。
這種類型的恢復有時稱為"分析器再同步"。error
標記扮演了表示所有錯誤標記的通配符的角色,而緊隨其后的標記扮演了同步標記的角色。
重要的一個說明是,通常error
不會作為語法規則的最后一個標記,像這樣:
def p_statement_print_error(p):
'statement : PRINT error'
print "Syntax error in print statement. Bad expression"
這是因為,第一個導致錯誤的標記會使得該規則立刻歸約,進而使得在后面還有錯誤標記的情況下,恢復變得困難。
6.8.2 悲觀恢復模式
另一個錯誤恢復方法是采用“悲觀模式”:該模式下,開始放棄剩余的標記,直到能夠達到一個合適的恢復機會。
悲觀恢復模式都是在p_error()
方法中做到的。例如,這個方法在開始丟棄標記后,直到找到閉合的'}',才重置分析器到初始化狀態:
def p_error(p):
print "Whoa. You are seriously hosed."
# Read ahead looking for a closing '}'
while 1:
tok = yacc.token() # Get the next token
if not tok or tok.type == 'RBRACE': break
yacc.restart()
下面這個方法簡單的拋棄錯誤的標記,並告知分析器錯誤被接受了:
def p_error(p):
print "Syntax error at token", p.type
# Just discard the token and tell the parser it's okay.
yacc.errok()
在p_error()方法中,有三個可用的方法來控制分析器的行為:
yacc.errok()
這個方法將分析器從恢復模式切換回正常模式。這會使得不會產生error
標記,並重置內部的error
計數器,而且下一個語法錯誤會再次產生p_error()
調用yacc.token()
這個方法用於得到下一個標記yacc.restart()
這個方法拋棄當前整個分析棧,並重置分析器為起始狀態
注意:這三個方法只能在p_error()
中使用,不能用在其他任何地方。
p_error()
方法也可以返回標記,這樣能夠控制將哪個標記作為下一個標記返回給分析器。這對於需要同步一些特殊標記的時候有用,比如:
def p_error(p):
# Read ahead looking for a terminating ";"
while 1:
tok = yacc.token() # Get the next token
if not tok or tok.type == 'SEMI': break
yacc.errok()
# Return SEMI to the parser as the next lookahead token
return tok
6.8.3 從產生式中拋出錯誤
如果有需要的話,產生式規則可以主動的使分析器進入恢復模式。這是通過拋出SyntacError
異常做到的:
def p_production(p):
'production : some production ...'
raise SyntaxError
raise SyntaxError
錯誤的效果就如同當前的標記是錯誤標記一樣。因此,當你這么做的話,最后一個標記將被彈出棧,當前的下一個標記將是error
標記,分析器進入恢復模式,試圖歸約滿足error
標記的規則。此后的步驟與檢測到語法錯誤的情況是完全一樣的,p_error()
也會被調用。
手動設置錯誤有個重要的方面,就是p_error()
方法在這種情況下不會調用。如果你希望記錄錯誤,確保在拋出SyntaxError
錯誤的產生式中實現。
注:這個功能是為了模仿yacc中的YYERROR
宏的行為
6.8.4 錯誤恢復總結
對於通常的語言,使用error
規則和再同步標記可能是最合理的手段。這是因為你可以將語法設計成在一個相對容易恢復和繼續分析的點捕獲錯誤。悲觀恢復模式只在一些十分特殊的應用中有用,這些應用往往需要丟棄掉大量輸入,再尋找合理的同步點。
6.9 行號和位置的跟蹤
位置跟蹤通常是個設計編譯器時的技巧性玩意兒。默認情況下,PLY跟蹤所有標記的行號和位置,這些信息可以這樣得到:
-
p.lineno(num)
返回第num個符號的行號 -
p.lexpos(num)
返回第num個符號的詞法位置偏移
例如:
def p_expression(p):
'expression : expression PLUS expression'
p.lineno(1) # Line number of the left expression
p.lineno(2) # line number of the PLUS operator
p.lineno(3) # line number of the right expression
...
start,end = p.linespan(3) # Start,end lines of the right expression
starti,endi = p.lexspan(3) # Start,end positions of right expression
注意:lexspan()
方法只會返回的結束位置是最后一個符號的起始位置。
雖然,PLY對所有符號的行號和位置的跟蹤很管用,但經常是不必要的。例如,你僅僅是在錯誤信息中使用行號,你通常可以僅僅使用關鍵標記的信息,比如:
def p_bad_func(p):
'funccall : fname LPAREN error RPAREN'
# Line number reported from LPAREN token
print "Bad function call at line", p.lineno(2)
類似的,為了改善性能,你可以有選擇性的將行號信息在必要的時候進行傳遞,這是通過p.set_lineno()
實現的,例如:
def p_fname(p):
'fname : ID'
p[0] = p[1]
p.set_lineno(0,p.lineno(1))
對於已經完成分析的規則,PLY不會保留行號信息,如果你是在構建抽象語法樹而且需要行號,你應該確保行號保留在樹上。
6.10 構造抽象語法樹
yacc.py沒有構造抽像語法樹的特殊方法。不過,你可以自己很簡單的構造出來。
一個最為簡單的構造方法是為每個語法規則創建元組或者字典,並傳遞它們。有很多中可行的方案,下面是一個例子:
def p_expression_binop(p):
'''expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression'''
p[0] = ('binary-expression',p[2],p[1],p[3])
def p_expression_group(p):
'expression : LPAREN expression RPAREN'
p[0] = ('group-expression',p[2])
def p_expression_number(p):
'expression : NUMBER'
p[0] = ('number-expression',p[1])
另一種方法可以是為不同的抽象樹節點創建一系列的數據結構,並賦值給p[0]
:
class Expr: pass
class BinOp(Expr):
def __init__(self,left,op,right):
self.type = "binop"
self.left = left
self.right = right
self.op = op
class Number(Expr):
def __init__(self,value):
self.type = "number"
self.value = value
def p_expression_binop(p):
'''expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression'''
p[0] = BinOp(p[1],p[2],p[3])
def p_expression_group(p):
'expression : LPAREN expression RPAREN'
p[0] = p[2]
def p_expression_number(p):
'expression : NUMBER'
p[0] = Number(p[1])
這種方式的好處是在處理復雜語義時比較簡單:類型檢查、代碼生成、以及其他針對樹節點的功能。
為了簡化樹的遍歷,可以創建一個通用的樹節點結構,例如:
class Node:
def __init__(self,type,children=None,leaf=None):
self.type = type
if children:
self.children = children
else:
self.children = [ ]
self.leaf = leaf
def p_expression_binop(p):
'''expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression'''
p[0] = Node("binop", [p[1],p[3]], p[2])
6.11 嵌入式動作
yacc使用的分析技術只允許在規則規約后執行動作。假設有如下規則:
def p_foo(p):
"foo : A B C D"
print "Parsed a foo", p[1],p[2],p[3],p[4]
方法只會在符號A,B,C和D都完成后才能執行。可是有的時候,在中間階段執行一小段代碼是有用的。假如,你想在A完成后立即執行一些動作,像下面這樣用空規則:
def p_foo(p):
"foo : A seen_A B C D"
print "Parsed a foo", p[1],p[3],p[4],p[5]
print "seen_A returned", p[2]
def p_seen_A(p):
"seen_A :"
print "Saw an A = ", p[-1] # Access grammar symbol to left
p[0] = some_value # Assign value to seen_A
在這個例子中,空規則seen_A
將在A移進分析棧后立即執行。p[-1]
指代的是在分析棧上緊跟在seen_A左側的符號。在這個例子中,是A符號。像其他普通的規則一樣,在嵌入式行為中也可以通過為p[0]
賦值來返回某些值。
使用嵌入式動作可能會導致移進歸約沖突,比如,下面的語法是沒有沖突的:
def p_foo(p):
"""foo : abcd
| abcx"""
def p_abcd(p):
"abcd : A B C D"
def p_abcx(p):
"abcx : A B C X"
可是,如果像這樣插入一個嵌入式動作:
def p_foo(p):
"""foo : abcd
| abcx"""
def p_abcd(p):
"abcd : A B C D"
def p_abcx(p):
"abcx : A B seen_AB C X"
def p_seen_AB(p):
"seen_AB :"
會產生移進歸約沖,只是由於對於兩個規則abcd和abcx中的C,分析器既可以根據abcd規則移進,也可以根據abcx規則先將空的seen_AB
歸約。
嵌入動作的一般用於分析以外的控制,比如為本地變量定義作用於。對於C語言:
def p_statements_block(p):
"statements: LBRACE new_scope statements RBRACE"""
# Action code
...
pop_scope() # Return to previous scope
def p_new_scope(p):
"new_scope :"
# Create a new scope for local variables
s = new_scope()
push_scope(s)
...
在這個例子中,new_scope作為嵌入式行為,在左大括號{
之后立即執行。可以是調正內部符號表或者其他方面。statements_block
一完成,代碼可能會撤銷在嵌入動作時的操作(比如,pop_scope())
6.12 Yacc的其他
- 默認的分析方法是LALR,使用SLR請像這樣運行
yacc():yacc.yacc(method="SLR")
注意:LRLR生成的分析表大約要比SLR的大兩倍。解析的性能沒有本質的區別,因為代碼是一樣的。由於LALR能力稍強,所以更多的用於復雜的語法。 - 默認情況下,yacc.py依賴lex.py產生的標記。不過,可以用一個等價的詞法標記生成器代替:
yacc.parse(lexer=x)
這個例子中,x必須是一個Lexer
對象,至少擁有x.token()
方法用來獲取標記。如果將輸入字串提供給yacc.parse()
,lexer還必須具有x.input()
方法。 - 默認情況下,yacc在調試模式下生成分析表(會生成parser.out文件和其他東西),使用
yacc.yacc(debug=0)
禁用調試模式。 - 改變parsetab.py的文件名:
yacc.yacc(tabmodule="foo")
- 改變parsetab.py的生成目錄:
yacc.yacc(tabmodule="foo",outputdir="somedirectory")
- 不生成分析表:
yacc.yacc(write_tables=0)
。注意:如果禁用分析表生成,yacc()將在每次運行的時候重新構建分析表(這里耗費的時候取決於語法文件的規模) - 想在分析過程中輸出豐富的調試信息,使用:
yacc.parse(debug=1)
- yacc.yacc()方法會返回分析器對象,如果你想在一個程序中支持多個分析器:
注意:p = yacc.yacc() ... p.parse()
yacc.parse()
方法只綁定到最新創建的分析器對象上。 - 由於生成生成LALR分析表相對開銷較大,先前生成的分析表會被緩存和重用。判斷是否重新生成的依據是對所有的語法規則和優先級規則進行MD5校驗,只有不匹配時才會重新生成。生成分析表是合理有效的辦法,即使是面對上百個規則和狀態的語法。對於復雜的編程語言,像C語言,在一些慢的機器上生成分析表可能要花費30-60秒,請耐心。
- 由於LR分析過程是基於分析表的,分析器的性能很大程度上取決於語法的規模。最大的瓶頸可能是詞法分析器和語法規則的復雜度。
7 多個語法和詞法分析器
在高級的分析器程序中,你可能同時需要多個語法和詞法分析器。
依照規則行事不會有問題。不過,你需要小心確定所有東西都正確的綁定(hooked up)了。首先,保證將lex()和yacc()返回的對象保存起來:
lexer = lex.lex() # Return lexer object
parser = yacc.yacc() # Return parser object
接着,在解析時,確保給parse()
方法一個正確的lexer引用:
parser.parse(text,lexer=lexer)
如果遺漏這一步,分析器會使用最新創建的lexer對象,這可能不是你希望的。
詞法器和語法器的方法中也可以訪問這些對象。在詞法器中,標記的lexer屬性指代的是當前觸發規則的詞法器對象:
def t_NUMBER(t):
r'\d+'
...
print t.lexer # Show lexer object
在語法器中,lexer和parser屬性指代的是對應的詞法器對象和語法器對象
def p_expr_plus(p):
'expr : expr PLUS expr'
...
print p.parser # Show parser object
print p.lexer # Show lexer object
如果有必要,lexer對象和parser對象都可以附加其他屬性。例如,你想要有不同的解析器狀態,可以為為parser對象附加更多的屬性,並在后面用到它們。
8 使用Python的優化模式
由於PLY從文檔字串中獲取信息,語法解析和詞法分析信息必須通過正常模式下的Python解釋器得到(不帶有-O或者-OO選項)。不過,如果你像這樣指定optimize
模式:
lex.lex(optimize=1)
yacc.yacc(optimize=1)
PLY可以在下次執行,在Python的優化模式下執行。但你必須確保第一次執行是在Python的正常模式下進行,一旦詞法分析表和語法分析表生成一次后,在Python優化模式下執行,PLY會使用生成好的分析表而不再需要文檔字串。
注意:在優化模式下執行PLY會禁用很多錯誤檢查機制。你應該只在程序穩定后,不再需要調試的情況下這樣做。使用優化模式的目的應該是大幅減少你的編譯器的啟動時間(萬事俱備只欠東風時)
9 高級調試
調試一個編譯器不是件容易的事情。PLY提供了一些高級的調試能力,這是通過Python的logging
模塊實現的,下面兩節介紹這一主題:
9.1 調試lex()和yacc()命令
lex()
和yacc()
命令都有調試模式,可以通過debug標識實現:
lex.lex(debug=True)
yacc.yacc(debug=True)
正常情況下,調試不僅輸出標准錯誤,對於yacc(),還會給出parser.out文件。這些輸出可以通過提供logging對象來精細的控制。下面這個例子增加了對調試信息來源的輸出:
# Set up a logging object
import logging
logging.basicConfig(
level = logging.DEBUG,
filename = "parselog.txt",
filemode = "w",
format = "%(filename)10s:%(lineno)4d:%(message)s"
)
log = logging.getLogger()
lex.lex(debug=True,debuglog=log)
yacc.yacc(debug=True,debuglog=log)
如果你提供一個自定義的logger,大量的調試信息可以通過分級來控制。典型的是將調試信息分為DEBUG
,INFO
,或者WARNING
三個級別。
PLY的錯誤和警告信息通過日志接口提供,可以從errorlog
參數中傳入日志對象
lex.lex(errorlog=log)
yacc.yacc(errorlog=log)
如果想完全過濾掉警告信息,你除了可以使用帶級別過濾功能的日志對象,也可以使用lex和yacc模塊都內建的Nulllogger
對象。例如:
yacc.yacc(errorlog=yacc.NullLogger())
9.2 運行時調試
為分析器指定debug選項,可以激活語法分析器的運行時調試功能。這個選項可以是整數(表示對調試功能是開還是關),也可以是logger對象。例如:
log = logging.getLogger()
parser.parse(input,debug=log)
如果傳入日志對象的話,你可以使用其級別過濾功能來控制內容的輸出。INFO
級別用來產生歸約信息;DEBUG
級別會顯示分析棧的信息、移進的標記和其他詳細信息。ERROR
級別顯示分析過程中的錯誤相關信息。
對於每個復雜的問題,你應該用日志對象,以便輸出重定向到文件中,進而方便在執行結束后檢查。
10 如何繼續
PLY分發包中的example
目錄包含幾個簡單的示例。對於理論性的東西以及LR分析發的實現細節,應當從編譯器相關的書籍中學習。
token | 標記 |
context free grammar | 上下文無關文法 |
syntax directed translation | 語法制導的翻譯 |
ambiguity | 二義 |
terminals | 終結符 |
non-terminals | 非終結符 |
documentation string | 文檔字符串(python中的_docstring_) |
shift-reduce | 移進-歸約 |
Empty Productions | 空產生式 |
Panic mode recovery | 悲觀恢復模式 |