【问题标题】:Left/Right recursion and Bison parsing stack behavior左/右递归和 Bison 解析堆栈行为
【发布时间】:2018-02-04 04:06:03
【问题描述】:

因此,在我

举个例子(只做加减的解析器):

扫描仪:

%%
[0-9]+ {return NUMBER;}
%%

解析器:

%%
/* Left */
expression:
    NUMBER
  | expression '+' NUMBER { $$ = $1 + $3; }
  | expression '-' NUMBER { $$ = $1 - $3; }
  ;
/* Right */
expression:
    NUMBER
  | NUMBER '+' expression { $$ = $1 + $3; }
  | NUMBER '-' expression { $$ = $1 - $3; }
  ;
%%

对于 1+5-2 的示例,似乎使用左递归,解析器从词法分析器接收到 '1' 并看到 '1' 匹配 expression: NUMBER 并将值 1 的表达式推送到解析器堆栈。它看到 + 并推动。然后它看到 5 和表达式 (1),+ 和 5 匹配 expression: expression '+' NUMBER,所以它弹出两次,进行数学运算并将值 6 的新表达式压入堆栈,然后重复减法。在任何一点,堆栈上最多有 3 个符号。所以它就像一个就地计算,从左到右运行。

使用正确的递归,我不确定为什么它必须加载堆栈上的所有符号,但我将尝试描述为什么会出现这种情况。它看到 1 并匹配 expression: NUMBER,因此它将值为 1 的表达式压入堆栈。它将“+”压入堆栈。当它看到 5 时,我的第一个想法是 5 本身可以匹配 expression: NUMBER 并因此成为值 5 的表达式,然后它加上堆栈上的最后两个符号可以匹配 expression: NUMBER '+' expression 但我的假设是因为expression 位于规则的右侧,它不能急于求成并将 5 作为表达式评估为 NUMBER,因为使用 LALR(1),它已经知道更多符号即将到来,因此它必须等到它命中列表的末尾?

TL;DR;

有人可以详细解释一下 Bison 如何管理其解析堆栈,以及它如何使用解析器语法规则进行移位/归约?欢迎使用愚蠢/人为的例子!

【问题讨论】:

    标签: recursion bison flex-lexer yacc lex


    【解决方案1】:

    使用 LR(自下而上)解析,每个非终结符在遇到其最后一个标记时精确地减少。 (LALR 解析是一种简化的 LR 解析,它处理前瞻的精确度略低。)在减少非终端之前,它的所有组件都存在于堆栈中。所以如果你使用正确的递归并且你正在解析

    NUMBER + NUMBER + NUMBER + NUMBER
    

    直到你到达结尾才会开始减少,因为每个NUMBER 都以expression 开头,而所有表达式都以最后一个NUMBER 结尾。

    如果您使用左递归,每个NUMBER 都会终止一个expression,因此每次遇到NUMBER 时都会进行缩减。

    但这不是使用左递归的原因。您使用左递归是因为它准确地描述了语言。如果你有7 - 2 - 1,你希望结果为4,因为这是代数规则所要求的:表达式被解析为(7 - 2) - 1,所以7 - 2必须首先减少。使用右递归,您会错误地将其计算为 6,因为 2 - 1 会首先减少。

    大多数运算符都与左侧相关联,因此您使用左递归。对于与右关联的偶发运算符,您需要右递归,并且您必须忍受堆栈增长。没什么大不了的。您的机器有大量内存。

    例如,考虑分配。 a = b = 42 表示 a = (b = 42)。如果您以关联方式进行操作,则首先将a 设置为b,然后尝试将某些内容设置为42; (a = b) = 42 在大多数语言中都没有意义,这当然不是预期的操作。


    LL(自上而下)解析使用前瞻来预测哪些生产将减少。它根本无法处理左递归,因为预测以递归循环结束:expressionexpression 开头,以expression 开头……并且解析器永远无法预测NUMBER。因此,对于 LL 解析器,您必须使用右递归,然后您的语法无法正确描述该语言(假设该语言具有左关联运算符,通常是这种情况)。有些人不介意这一点,但我认为语法实际上应该指示正确的解析,并且我发现使自顶向下解析器可解析的语法变得混乱且难以阅读所需的修改。您的里程可能会有所不同。


    顺便说一句,“强迫你的喉咙”是对文档的一种非常粗鲁的描述,它试图给你很好的建议。持怀疑态度是件好事——如果你努力弄清楚它们为什么会这样工作,你会更好地理解事情——但很多人只是想要好的建议。

    【讨论】:

    【解决方案2】:

    所以在阅读了 bison 文档中这个相当重要的页面之后:

    https://www.gnu.org/software/bison/manual/html_node/Lookahead.html#Lookahead

    结合run with with

    %debug
    

    yydebug = 1;
    

    在我的main()

    我想我确切地看到了正在发生的事情。

    使用左递归,它会看到 1 并将其移位。前瞻现在是 +。然后它确定它可以通过expression: NUMBER将1减少到expression。所以它会弹出并在堆栈上放置一个expression。它移动 + 和 NUMBER(5),然后看到它可以通过 expression: expression '+' NUMBER 减少并弹出 3x 并推送一个新的表达式 (6)。基本上,通过使用前瞻和规则,bison 可以在读取令牌时随时确定是否需要移动或减少。如此重复,解析堆栈上最多有 3 个符号/分组(对于这个简化的表达式评估部分)。

    使用右递归,它会看到一个 1 并将其移位。前瞻现在是 +。解析器认为没有理由将 1 减少为表达式(1),因为没有规则可以使用 expression '+',因此它只是继续移动每个标记,直到它到达输入的末尾。此时,堆栈上有 [NUMBER, +, NUMBER, -, NUMBER] 所以它看到最近的数字可以减少到表达式(2)然后移动它。然后开始应用规则(`expression: NUMBER '-' expression')等等。

    因此,我理解的关键是 Bison 使用前瞻令牌做出明智的决定,即现在减少还是仅根据其掌握的规则进行转移。

    【讨论】:

    • 这非常接近。解析集确实使用前瞻来决定是移位还是减少。但是减少只能在整个生产右侧被读取的那一刻准确地发生,不会很快(显然,不是吗?)也不会更晚。使用右递归,所有嵌套的递归扩展都捆绑在输入的末尾。通过左递归,它们聚集在左边。每个产生式的跨度与解析算法的解析能力无关,而且确实有不同的方法可以做到这一点,所有这些方法都在输入的同一点执行相同的缩减。
    猜你喜欢
    • 1970-01-01
    • 2015-08-03
    • 1970-01-01
    • 1970-01-01
    • 2021-03-23
    • 2017-11-27
    • 1970-01-01
    • 2015-04-04
    • 2023-03-27
    相关资源
    最近更新 更多