使用shift-reduce解析构建解析树

时间:2014-01-11 16:12:10

标签: parsing tree programming-languages lr

我正在尝试解析我的空闲时间,我想为一个非常简单的语法实现一个shift-reduce解析器。我已经阅读了很多在线文章,但我仍然对如何创建解析树感到困惑。以下是我想要做的一个例子:


语法:

Expr -> Expr TKN_Op Expr 
Expr -> TKN_Num

以下是输入示例:

1 + 1 + 1 + 1

在标记化之后,变为:

TKN_Num TKN_Op TKN_Num TKN_Op TKN_Num TKN_Op TKN_Num

我理解:

  1. 转移表示在堆栈上推送第一个输入令牌并将其从输入中删除
  2. 缩减表示用语法元素替换堆栈中的一个或多个元素
  3. 所以,基本上,这应该发生:

    Step 1:
        Stack:
        Input: TKN_Num TKN_Op TKN_Num TKN_Op TKN_Num TKN_Op TKN_Num
        What: Stack is empty. Shift.
    
    Step 2:
        Stack: TKN_Num
        Input: TKN_Op TKN_Num TKN_Op TKN_Num TKN_Op TKN_Num
        What: TKN_Num can be reduced to Expr. Reduce.
    
    Step 3:
        Stack: Expr
        Input: TKN_Op TKN_Num TKN_Op TKN_Num TKN_Op TKN_Num
        What: Cannot reduce. Shift.
    
    Step 4:
        Stack: Expr TKN_Op 
        Input: TKN_Num TKN_Op TKN_Num TKN_Op TKN_Num
        What: Cannot reduce. Shift.
    
    Step 5:
        Stack: Expr TKN_Op TKN_Num
        Input: TKN_Op TKN_Num TKN_Op TKN_Num
        What: TKN_Num can be reduced to Expr. Reduce.
        // What should I check for reduction? 
        // Should I try to reduce incrementally using 
        // only the top of the stack first, 
        // then adding more stack elements if I couldn't
        // reduce the top alone?
    
    Step 6:
        Stack: Expr TKN_Op Expr
        Input: TKN_Op TKN_Num TKN_Op TKN_Num
        What: Expr TKN_Op Expr can be reduced to Expr. Reduce.
    
    Step 7:
        Stack: Expr
        Input: TKN_Op TKN_Num TKN_Op TKN_Num
        What: ...
        // And so on...
    

    除了“要减少什么?”之外,我不知道如何正确构建解析树。树应该看起来像这样:

    1    +    o
              |
         1    +    o
                   |
              1    +    1
    

    我应该在减少时创建一个新节点吗?

    我应该何时将子项添加到新创建的节点/何时应该创建新的根节点?

2 个答案:

答案 0 :(得分:3)

简单明了的做法是在每次缩减时创建一个树节点,并从缩减到该树节点的语法元素中添加树节点。

使用与原始解析器使用的“shift token”堆栈并行运行的节点堆栈可以轻松管理。对于长度为N的规则的每次减少,移位令牌堆栈缩短N,并且非移位令牌被推送到移位堆栈。同时,通过删除前N个节点缩短节点堆栈,为非终结点创建节点,将删除的N个节点作为子节点附加,并将该节点推送到节点堆栈。

此策略甚至适用于具有零长度右侧的规则:创建树节点并将空的子集附加到其上(例如,创建叶节点)。

如果您认为终端节点上的“移位”是(终端节点的字符)减少到终端节点,则终端节点正好适合。为终端创建节点,并将其推到堆栈。

如果你这样做,你得到一个“具体语法/解析树”,它与同形语法相匹配。 (我们为我提供的商业工具这样做)。有很多人不喜欢这样的具体树,因为它们包含关键字等的节点,这些节点并没有真正增加太多价值。没错,但是这样的树构造起来非常容易,并且非常容易理解,因为语法树结构。当你有2500条规则时(正如我们对完整的COBOL解析器所做的那样),这很重要。这也很方便,因为所有机制都可以完全构建到解析基础结构中。语法工程师只是编写规则,解析器运行,瞧,树。更改语法也很容易:只需更改它,瞧,你仍然会得到解析树。

但是,如果你不想要一个具体的树,例如,你想要一个“抽象语法树”,那么你要做的就是让语法工程师控制哪些缩减产生节点;通常是在每个语法规则上添加一些程序附件(代码),以便在缩减步骤中执行。然后,如果任何此类过程附件生成节点,则它将保留在节点堆栈上。产生节点的任何程序附件必须附加由右手元素产生的节点。如果这是YACC / Bison / ...大多数shift-reduce解析器引擎都做的。去阅读关于Yacc或Bison并检查语法。这个方案给你很多控制权,代价是坚持你采取这种控制。 (对于我们的工作,我们不希望在构建语法方面付出太多的工程努力。)

在生成CST的情况下,从树中删除“无用”节点在概念上是直截了当的;我们在我们的工具中这样做。结果很像AST,没有手动编写所有这些程序附件。

答案 1 :(得分:2)

您遇到麻烦的原因是您的语法中存在转换/减少冲突:

expr: expr OP expr
    | number

您可以通过两种方式解决此问题:

expr: expr OP number
    | number

表示左关联运算符,或

expr: number OP expr
    | number

用于右关联的。这也应该决定树的形状。

通常在检测到一个子句完成时进行减少。在右关联的情况下,您将从期望数字的状态1开始,将其推送到值堆栈并切换到状态2.在状态2中,如果令牌不是OP,则可以将数字减少为expr 。否则,按下操作符并切换到状态1.状态1完成后,您可以将数字,运算符和表达式减少到另一个表达式。注意,你需要一种机制来在减少后“返回”。然后整个解析器将从状态0开始,比如说,它立即进入状态1并在减少后接受。

请注意,像yacc或bison这样的工具可以让这种东西变得更加容易,因为它们带来了所有低级机器和堆栈。