在递归下降解析器中实现“剪切”

时间:2013-01-03 18:15:17

标签: parsing prolog backtracking peg prolog-cut

我正在Python中实现一个PEG解析器生成器,到目前为止我已经取得了成功,除了“剪切”功能,其中任何人都知道Prolog必须知道。

这个想法是在解析了一个cut(!)符号后,不应该在同一级别尝试其他选项。

expre = '(' ! list ')' | atom.

表示在看到(之后,解析必须成功,或者在不尝试第二个选项的情况下失败。

我正在使用Python的(非常有效的)异常系统来强制回溯,所以我尝试了一个特殊的FailedCut异常,它会中止封闭的选择,但这不起作用。

任何指向如何在其他解析器生成器中实现此功能的指针都会有所帮助。

也许我遇到的问题是缺乏地方性。为规则的左侧部分生成的代码将类似于:

cut_seen = False
try:
    self.token('(')
    cut_seen = True 
    self.call('list')
    self.token(')')
except FailedParse as e:
    if cut_seen:
         raise FailedCut(e)
    raise

然后,为选择(|)运算符生成的代码将在捕获FailedCut时跳过以下选项。我所说的缺少地方性的意思是,捕捉FailedCut的选择可能会在调用中深入,因此影响太难以辨别。

不是为序列生成代码,而是尝试通知封闭的 cut 选项,我可以让选择生成的代码提防它们。这将使裁剪的范围非常局部,与Prolog不同,但足以满足我在PEG解析器中的需要,即在看到某个令牌序列后提交选项,因此错误报告指的是该位置在源中,而不是另一个可能有其他选项的位置。

我刚刚想到,如果为规则/谓词生成的代码捕获FailedCut并将其转换为正常的FailedParse异常,那么削减将具有正确的范围。

关于@false的问题,这里有一个我想要工作的完整例子:

start = expre ;

expre = named | term ;

named = word ':' ! term;

term = word ;

在该语法中,word可以通过namedterm到达,但我希望解析器在看到named分支后提交:分支{1}}。

解决方案

公平地说,到目前为止我已经在https://bitbucket.org/apalala/grako/发表了我的作品。

在最终解决方案中,使用此上下文管理器附带序列:

@contextmanager
def _sequence(self):
    self._push_cut()
    try:
        yield
    except FailedParse as e:
        if self._cut():
            self.error(e, FailedCut)
        else:
            raise
    finally:
        self._pop_cut()

选择函数中的选项随附:

@contextmanager
def _option(self):
    p = self._pos
    try:
        self._push_ast()
        try:
            yield
            ast = self.ast
        finally:
            self._pop_ast()
        self.ast.update(ast)
    except FailedCut as e:
        self._goto(p)
        raise e.nested
    except FailedParse:
        self._goto(p)

强制退出选择而不是返回以尝试下一个选项。

切割本身就这样实现了:

def _cut(self):
    self._cut_stack[-1] = True

完整的源代码可以在Bitbucket找到。

3 个答案:

答案 0 :(得分:3)

在Prolog中使用ISO Prolog的异常处理(catch/3throw/1),可以实现切割:

cut. % Simply succeeds
cut :-
   throw(cut). % on backtracking throws an exception

这需要在适当的地方捕获该异常。例如,用户定义的谓词的每个目标(即非终端)现在都可以包含:

catchcut(Goal) :-
   catch(Goal,cut,fail).

这不是实现切割的最有效方法,因为!成功后它不会释放资源,但它可能足以满足您的需要。此外,此方法现在可能会干扰用户定义的catch/3用法。但是你可能不想在任何情况下模仿整个Prolog语言。

另外,请考虑直接使用Prolog的 - 语法。当用另一种语言实现这一点时,有很多细则不明显。

答案 1 :(得分:2)

我的问题最后提出的解决方案有效:

cut_seen = False
try:
    self.token('(')
    cut_seen = True 
    self.call('list')
    self.token(')')
except FailedParse as e:
    if cut_seen:
         raise FailedCut(e)
    raise

然后,无论何时评估选项或可选项,代码都如下所示:

p = self.pos
try:
   # code for the expression
except FailedCut:
    raise
except FailedParse:
    self.goto(p)

修改

实际解决方案需要保持“切割堆栈”。 source code是int Bitbucket。

答案 2 :(得分:1)

请阅读。

我建议使用深cut_seen(与修改解析器的状态一样)以及带有局部变量的保存和恢复状态。这使用线程的堆栈作为" cut_seen堆栈"。

但是你有另一个解决方案,我很确定你已经很好了。

顺便说一句:好的编译器 - 它与我用pyPEG做的正好相反,所以我可以学到很多东西; - )