4.18 基于条件的扫描和启动条件
最后更新于:2022-04-01 01:10:08
在高级的分析器应用程序中,使用状态化的词法扫描是很有用的。比如,你想在出现特定标记或句子结构的时候触发开始一个不同的词法分析逻辑。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’,与此同时,词法分析的状态退回到初始状态。