坚持将cfg翻译成dcg

时间:2015-03-31 11:53:51

标签: prolog context-free-grammar dcg

我试图自学prolog并为一个简单的算术cfg实现一个解释器:

<expression> --> number
<expression> --> ( <expression> )
<expression> --> <expression> + <expression>
<expression> --> <expression> - <expression>
<expression> --> <expression> * <expression>
<expression> --> <expression> / <expression> 

到目前为止,我已经在swi-prolog中写了这篇文章,这些文章遇到了下面描述的一些错误;

expression(N) --> number(Cs), { number_codes(N, Cs) }.
expression(N) --> "(", expression(N), ")".
expression(N) --> expression(X), "+", expression(Y), { N is X + Y }.
expression(N) --> expression(X), "-", expression(Y), { N is X - Y }.

number([D|Ds]) --> digit(D), number(Ds).
number([D])    --> digit(D).

digit(D) --> [D], { code_type(D, digit) }.

使用

进行测试
phrase(expression(X), "12+4"). 

给出X = 16,这是好的。还

phrase(expression(X), "(12+4)"). 

作品和短语(表达式(X),&#34; 12 + 4 + 5&#34;)。没关系。

但是尝试

phrase(expression(X), "12-4"). 

导致&#34;错误:超出本地堆栈&#34;除非我注释掉&#34; +&#34;规则。虽然我可以添加两个以上的数字,但是括号不能递归地工作(即&#34;(1 + 2)+ 3&#34;挂起)。

我确信解决方案很简单,但我还没有能够从我发现的在线教程中找到答案。

1 个答案:

答案 0 :(得分:1)

你所做的一切原则上都是正确的。你是对的:答案很简单。

但是

左递归在明确条款语法中是致命的;症状正是你所看到的行为。

如果在expression上设置间谍点并使用跟踪工具,则可以看到堆栈增长,增长和增长,而解析器根本没有进展。

gtrace.
spy(expression).
phrase(expression(N),"12-4").

如果您仔细考虑Prolog执行模型,您可以看到发生了什么。

  1. 我们尝试解析&#34; 12-4&#34;作为表达。

    我们的调用堆栈包含来自第1步的expression调用,我将编写expression(1)。

  2. 我们成功解析了&#34; 12&#34;作为表达式,通过&#34;表达式#34;的第一个子句,我们记录一个选择点,以防我们稍后需要回溯。实际上我们需要立即回溯,因为涉及phrase的父请求说我们要解析整个字符串,而我们还没有:我们仍然有&#34; -4&#34;去。所以我们失败了,回到选择点。我们已经证明了&#34;表达式&#34;的第一个条款没有成功,所以我们重试第二个条款。

    调用堆栈:expression(1)。

  3. 我们尝试解析&#34; 12-4&#34;使用&#34;表达式#34;的第二个子句,但立即失败(初始字符不是&#34;(&#34;)。所以我们失败并重试第三个句子。

    调用堆栈:expression(1)。

  4. 第三个句子要求我们在输入的开头解析一个表达式,然后找到一个&#34; +&#34;和另一个表达。所以我们现在必须尝试将输入的开头解析为表达式。

    调用堆栈:expression(4)expression(1)。

  5. 我们尝试解析&#34; 12-4&#34;作为一个表达式,并使用&#34; 12&#34;成功,就像在步骤1中一样。我们记录一个选择点,以防我们稍后需要回溯。

    调用堆栈:expression(4)expression(1)。

  6. 我们现在恢复在步骤4中开始尝试解析&#34; 12-4&#34;作为表达&#34;表达&#34;的第3条的表达。我们做了第一点;现在我们必须尝试解析&#34; -4&#34;作为&#34;表达&#34;的第3条右边的其余部分,即"+", expression(Y)。但&#34; - &#34;不是&#34; +&#34;,所以我们立即失败,然后回到最近记录的选择点,即步骤5中记录的选择点。接下来是尝试找到一种不同的解析方法。输入作为表达式。我们使用&#34;表达式&#34;。

    的第二个子句恢复此搜索

    调用堆栈:expression(4)expression(1)。

  7. 第二个条款再次失败。所以我们继续使用&#34;表达式#34;的第三个子句。这要求我们在输入的开头寻找一个表达式(作为确定我们当前的两个调用&#34;表达式&#34;是否可以成功或将失败的一部分)。所以我们称之为&#34;表达&#34;试。

    调用堆栈:expression(7)expression(4)expression(1)。

  8. 你可以看到,每次我们向堆栈添加对expression的调用时,我们都会成功,寻找加号,失败,然后再试一次,最后到达第三个子句,此时我们将在堆栈上再次调用,然后重试。

    简短回答:左递归在DCG中是致命的。

    它在递归下降解析器中也是致命的,并且解决方案大致相同:不要向左复发。

    你的语法的非左递归版本将是:

    expression(N) --> term(N).
    expression(N) --> term(X), "+", expression(Y), { N is X + Y }.
    expression(N) --> term(X), "-", expression(Y), { N is X - Y }.
    term(N) --> number(Cs), { number_codes(N, Cs) }.
    term(N) --> "(", expression(N), ")".
    

    然而,这使得&#34; - &#34;在很多情况下,需要重新解析初始术语,因此在生产代码中使用的常用方法是做一些不像你开始使用的BNF,更像下面的EBNF版本:

    expression = term {("+"|"-") term}
    term = number | "(" expression ")".
    

    我学会写它的方式(足够久以前,我不再记得为此归功于它)是这样的(我一开始觉得它很难看,但它会在你身上发展):

    expression(N) --> term(X), add_op_sequence(X,N).
    add_op_sequence(LHS0, Result) -->
        "+", term(Y),
        {LHS1 is LHS0 + Y},
        add_op_sequence(LHS1,Result).
    add_op_sequence(LHS0, Result) -->
        "-", term(Y),
        {LHS1 is LHS0 - Y},
        add_op_sequence(LHS1,Result).
    add_op_sequence(N,N) --> [].
    
    term(N) --> number(Cs), { number_codes(N, Cs) }.
    term(N) --> "(", expression(N), ")".
    

    到目前为止累积的值在add_op_sequence的左侧参数中传递,最终(当序列以空的生产结束时)作为结果传回。

    解析策略称为&#39;左角解析&#39;是一种处理这个问题的方法;关于在自然语言处理中使用Prolog的书籍几乎总是会讨论它。