[我正在尝试使用ply编写解析器,其中\n
在语法上有时很重要,有时必须忽略。
更准确地说,我想解析的语言中,有一些与定义相对应的行必须以\n
结尾以指示定义的结尾。在所有其他情况下,\n
必须忽略,仅用于对输入文件中的行进行计数。
例如:
def1
def2
def3
def4
将是有效的,但:
def1
def2 def3
def 4
不会,因为每个定义都必须以\n
结尾
我想要的有点类似于我们在Python中可以编写的内容:
def a(b):
if b==0:
return (b+1)
或
def a(b):
if b==0:
return (b+1)
但是
def a(b): if b==0: return (b+1)
不允许。\n
是指示语句结束所必需的,但对代码没有任何影响。
而且我不知道要用层板重现这种行为。如果我像这样定义令牌:
def t_NEWLINE(self,t):
r'\n+'
t.lexer.lineno += len(t.value)
return t
除非语法明确允许将此标记插入几乎所有位置,否则将不允许\n
。
我考虑过上下文语法,但是我的情况只有一个上下文。我只是想能够同时使用\n
作为令牌是某些规则,否则将被忽略。
有什么办法吗?
由于Ply为您提供了图灵完备的编程语言(Python)的强大功能,所以肯定会有一种方法。但是,如果不了解问题的具体细节,就不可能提供很多解决方案。
Python本身的词法分析确实需要一个更复杂的策略,该策略包括一个小型状态机(基本上是为了消除括号内的换行符,在括号中将其忽略)。请注意,即使是简单的Python语句也必须以换行符或分号终止,因此终止符肯定在语法中。典型的Python词法分析器会忽略注释和空白行。我可以提供一个示例,但我不知道它在这里是否有意义,因为您的语言显然仅“与Python中的语言有点类似”。
因此,我在这里竭尽全力去尝试一个用例,该用例适合您的问题中非常广泛的描述,并且在Ply中相对容易解决。我接受它可能与您的用例完全无关,但是它可能会为将来有不同但相似要求的读者使用。
实际上,遇到一种不需要任何形式的语句终止的语言实际上很少见,尽管这当然不是不可能的。例如,一种典型的语言包括
a = b
而不是例如let a = b
),除非语句有确定的终止符,否则将是不明确的。 (a(b)
可以是一个函数调用(一个语句)或两个连续的表达式语句;对于具有上述特征的大多数语言,可以构造类似的示例。
仍然,语言设计可以克服所有这些。这种设计最简单的方法是要求所有语句,甚至函数调用和赋值,都以关键字开头。大概使用这种语言,定义语句也以关键字开头,并且坚持使用围绕定义的换行符的唯一原因是美观。 (但是,美观是有道理的。它是美观而不是解析限制,这导致问题中的Python单行定义是非法的。)
然后,我们有一种语言,其中的定义语句以关键字开头,其符号为DEF
,以符号END
结尾(否则,我们将不知道定义的结尾)。并且我们还将假设赋值语句以关键字LET
开头,不需要显式终止。 (当然,会有其他语句类型,但是它们将遵循与LET
相同的模式。)无论出于何种原因,我们都想确保DEF
始终是行中的第一个标记,而END
则是行中的第一个标记始终是最后一个标记,尽管我们对LET a = b LET c = 3
感到满意,但它可以确保定义不会与其他任何语句水平并存。
执行此操作的一种方法是忽略换行符,除了DEF
之前或END
之后的换行。然后,我们将编写一个语法,其中包括:
lines : #empty
| lines line NEWLINE
line : #empty
| line simple_stmt
| definition
definition : DEF prototype lines END
simple_stmt : LET lhs '=' rhs
注意,以上语法要求程序为空或以NEWLINE结尾。
现在,要过滤掉不重要的NEWLINE
标记,我们可以在Ply生成的词法分析器周围使用包装器类。包装器的构造函数将一个词法分析器作为参数,并通过删除认为不重要的NEWLINE标记来过滤该词法分析器的输出流。我们还通过在必要时构造NEWLINE令牌来确保输入以NEWLINE结尾(除非它为空)。 (这实际上不是问题的一部分,但是它简化了语法。)
# Used to fabricate a token object.
from types import SimpleNamespace
class LexerWrapper(object):
def __init__(self, lexer):
"""Create a new wrapper given the lexer which is being wrapped"""
self.lexer = lexer
# None, or tuple containing queued token.
# Using a tuple allows None (eof) to be queued.
self.pending = None
# Previous token or None
self.previous = None
def token(self):
"""Return the next token, or None if end of input has been reached"""
# If there's a pending token, send it
if self.pending is not None:
t = self.pending[0]
self.pending = None
return t
# Get the next (useful) token
while True
t = self.lexer.token()
# Make sure that we send a NEWLINE before EOF
if t is None:
t, self.previous = self.previous, None
self.pending = (None,)
if t is not None and t.type != 'NEWLINE':
# Manufacture a NEWLINE token if necessary
t = SimpleNamespace( type='NEWLINE'
, value='\n'
, lineno=self.lexer.lineno
)
return t
elif t.type == 'NEWLINE':
if self.previous is None or self.previous.type == 'NEWLINE':
# Get another token
continue
if self.previous.type == 'END':
# Use this NEWLINE if it follows END
self.previous = None
else:
# Get another token
self.previous = t
continue
else:
self.previous = t
return t