在高級的分析器應用程序中,使用狀態化的詞法掃描是很有用的。比如,你想在出現特定標記或句子結構的時候觸發開始一個不同的詞法分析邏輯。PLY允許lexer在不同的狀態之間轉換。每個狀態可以包含一些自己獨特的標記和規則等。這是基于GNU flex的“啟動條件”來實現的,關于flex詳見[http://flex.sourceforge.net/manual/Start-Conditions.html#Start-Conditions](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’,與此同時,詞法分析的狀態退回到初始狀態。
- 0 一些翻譯約定
- 1 前言和預備
- 2 介紹
- 3 PLY概要
- 4 Lex
- 4.1 Lex的例子
- 4.2 標記列表
- 4.3 標記的規則
- 4.4 標記的值
- 4.5 丟棄標記
- 4.6 行號和位置信息
- 4.7 忽略字符
- 4.8 字面字符
- 4.9 錯誤處理
- 4.10 構建和使用lexer
- 4.11 @TOKEN裝飾器
- 4.12 優化模式
- 4.13 調試
- 4.14 其他方式定義詞法規則
- 4.15 額外狀態維護
- 4.16 Lexer克隆
- 4.17 Lexer的內部狀態
- 4.18 基于條件的掃描和啟動條件
- 4.19 其他問題
- 5 語法分析基礎
- 6 Yacc
- 6.1 一個例子
- 6.2 將語法規則合并
- 6.3 字面字符
- 6.4 空產生式
- 6.5 改變起始符號
- 6.6 處理二義文法
- 6.7 parser.out調試文件
- 6.8 處理語法錯誤
- 6.9 行號和位置的跟蹤
- 6.10 構造抽象語法樹
- 6.11 嵌入式動作
- 6.12 Yacc的其他
- 7 多個語法和詞法分析器
- 8 使用Python的優化模式
- 9 高級調試
- 9.1 調試lex()和yacc()命令
- 9.2 運行時調試
- 10 如何繼續