如何精确创建抽象语法树?


47

我想我了解AST的目标,并且之前我已经构建了几个树结构,但从未构建过AST。由于节点是文本而不是数字,所以我很困惑,因此在解析某些代码时,我想不出一种输入令牌/字符串的好方法。

例如,当我查看AST的图表时,变量及其值是等号的叶节点。这对我来说很有意义,但是我将如何实施呢?我想我可以视情况而定,以便当我偶然遇到“ =”时,我将其用作节点,并将在“ =”之前解析的值添加为叶子。这似乎是错误的,因为根据语法的不同,我可能不得不为成千上万的东西辩护。

然后我遇到了另一个问题,那棵树是如何横穿的?我会一直下降到最低点吗,当我到达最低点时又返回一个节点,并对它的邻居也这样做吗?

我已经看到了大量关于AST的图表,但是我找不到一个简单的代码示例,这可能会有所帮助。


您缺少的关键概念是递归。递归有点违反直觉,每个学习者最终都会“点击”它们时会有所不同,但是如果没有递归,就根本无法理解解析(以及很多其他计算主题)。
Kilian Foth,2014年

我得到了递归,我只是想在这种情况下很难实现它。我实际上想使用递归,但最终遇到了很多情况,这些情况不适用于一般解决方案。Gdhoward的答案现在对我有很大帮助。
Howcan

构建RPN计算器作为练习可能是练习。它不会回答您的问题,但可能会教一些必要的技能。

实际上,我之前已经构建了RPN计算器。答案对我很有帮助,我认为我现在可以进行基本的AST了。谢谢!
Howcan

Answers:


47

简短的答案是您使用堆栈。是一个很好的例子,但我将其应用于AST。

仅供参考,这是Edsger Dijkstra的Shunting-Yard算法

在这种情况下,我将使用运算符堆栈和表达式堆栈。由于数字在大多数语言中都被视为表达式,因此我将使用表达式堆栈来存储它们。

class ExprNode:
    char c
    ExprNode operand1
    ExprNode operand2

    ExprNode(char num):
        c = num
        operand1 = operand2 = nil

    Expr(char op, ExprNode e1, ExprNode e2):
        c = op
        operand1 = e1
        operand2 = e2

# Parser
ExprNode parse(string input):
    char c
    while (c = input.getNextChar()):
        if (c == '('):
            operatorStack.push(c)

        else if (c.isDigit()):
            exprStack.push(ExprNode(c))

        else if (c.isOperator()):
            while(operatorStack.top().precedence >= c.precedence):
                operator = operatorStack.pop()
                # Careful! The second operand was pushed last.
                e2 = exprStack.pop()
                e1 = exprStack.pop()
                exprStack.push(ExprNode(operator, e1, e2))

            operatorStack.push(c)

        else if (c == ')'):
            while (operatorStack.top() != '('):
                operator = operatorStack.pop()
                # Careful! The second operand was pushed last.
                e2 = exprStack.pop()
                e1 = exprStack.pop()
                exprStack.push(ExprNode(operator, e1, e2))

            # Pop the '(' off the operator stack.
            operatorStack.pop()

        else:
            error()
            return nil

    # There should only be one item on exprStack.
    # It's the root node, so we return it.
    return exprStack.pop()

(请对我的代码好一点。我知道它并不健壮;它只是伪代码。)

无论如何,从代码中可以看到,任意表达式可以是其他表达式的操作数。如果您输入以下内容:

5 * 3 + (4 + 2 % 2 * 8)

我编写的代码将产生此AST:

     +
    / \
   /   \
  *     +
 / \   / \
5   3 4   *
         / \
        %   8
       / \
      2   2

然后,当您要生成该AST的代码时,您可以执行Post Order Tree Traversal。当您访问带有数字的叶节点时,您将生成一个常量,因为编译器需要知道操作数的值。当您与操作员一起访问节点时,会从操作员生成适当的指令。例如,“ +”运算符为您提供“加”指令。


这适用于具有从左到右关联而不是从右到左的运算符。
西蒙(Simon)

@Simon,添加从右到左运算符的功能将非常简单。最简单的方法是添加一个查找表,如果运算符从右到左,则只需反转操作数的顺序即可。
加文·霍华德

4
@Simon如果您想同时支持这两种方法,则最好在其全部功能上查找调车场算法。随着算法的发展,这绝对是一个难题。
biziclop '16

19

在测试中通常如何描绘AST(在叶节点上具有数字/变量而在内部节点处具有符号的树)与实际实现方式之间,存在很大的区别。

AST(以OO语言表示)的典型实现方式大量使用了多态性。AST中的节点通常用各种类来实现,所有这些类都来自一个公共ASTNode类。对于您正在处理的语言中的每个句法构造,都会有一个用于在AST中表示该构造的类,例如ConstantNode(对于常量,例如0x1042),VariableNode(对于变量名),AssignmentNode(对于赋值操作),ExpressionNode(对于通用表达式)等。
每种特定的节点类型都指定该节点是否有子节点,多少个节点以及可能是哪种类型。一个ConstantNode遗嘱通常没有孩子,一个AssignmentNode遗嘱有两个孩子,一个ExpressionBlockNode可以有任何数目的孩子。

AST由解析器构建,解析器知道它刚刚解析了什么结构,因此它可以构建正确的AST节点。

遍历AST时,节点的多态性才真正发挥作用。该基础ASTNode定义了可以在节点上执行的操作,每种特定的节点类型都以特定的方式针对该特定的语言构造实现这些操作。


9

从源文本构建AST是“简单”的解析。究竟如何完成取决于解析的形式语言和实现。您可以使用解析器生成器,例如menhir(用于Ocaml),GNU bisonwith flexANTLR等。它通常通过对某些递归下降解析器进行编码来“手动”完成(请参阅此答案以解释原因)。解析的上下文方面通常在其他地方(符号表,属性等)完成。

但是,实际上AST比您认为的要复杂得多。例如,在像GCC这样的编译器中,AST保留源位置信息和一些键入信息。阅读有关GCC中的通用树的信息,并查看其gcc / tree.def内部。顺便说一句,请查看我设计和实现的GCC MELT内部,它与您的问题有关。


我正在制作一个Lua解释器来解析源文本并在JS中转换为数组。我可以认为它是AST吗?我应该做这样的事情: --My comment #1 print("Hello, ".."world.") 转换为`[{{“ type”:“-”,“ content”:“我的评论#1”},{“ type”:“ call”,“ name”:“ print“,” arguments“:[[{{” type“:” str“,” action“:” ..“,” content“:”你好,“},{” type“:” str“,” content“: “世界。” 我认为它在JS中比其他任何一种语言都简单得多!
Hydroper

@TheProHands这将被视为令牌,而不是AST。

2

我知道这个问题已经4岁多了,但是我觉得我应该添加一个更详细的答案。

抽象语法树的创建与其他树没有什么不同;在这种情况下,更正确的说法是语法树节点的节点数量各不相同。

一个例子是二进制表达式,例如:1 + 2 一个简单的表达式,它将创建一个单个的根节点,该节点包含一个左右节点,该节点保存有关数字的数据。在C语言中,它看起来像

struct ASTNode;
union SyntaxNode {
    int64_t         llVal;
    uint64_t        ullVal;
    struct {
        struct ASTNode *left, *right;
    } BinaryExpr;
};

enum SyntaxNodeType {
    AST_IntVal, AST_Add, AST_Sub, AST_Mul, AST_Div, AST_Mod,
};

struct ASTNode {
    union SyntaxNode *Data;
    enum SyntaxNodeType Type;
};

您的问题还在于如何遍历?在这种情况下,遍历称为“ 访问节点”。访问每个节点要求您使用每种节点类型来确定如何评估每个语法节点的数据。

这是C语言中的另一个示例,我只打印了每个节点的内容:

void AST_PrintNode(const ASTNode *node)
{
    if( !node )
        return;

    char *opername = NULL;
    switch( node->Type ) {
        case AST_IntVal:
            printf("AST Integer Literal - %lli\n", node->Data->llVal);
            break;
        case AST_Add:
            if( !opername )
                opername = "+";
        case AST_Sub:
            if( !opername )
                opername = "-";
        case AST_Mul:
            if( !opername )
                opername = "*";
        case AST_Div:
            if( !opername )
                opername = "/";
        case AST_Mod:
            if( !opername )
                opername = "%";
            printf("AST Binary Expr - Oper: \'%s\' Left:\'%p\' | Right:\'%p\'\n", opername, node->Data->BinaryExpr.left, node->Data->BinaryExpr.right);
            AST_PrintNode(node->Data->BinaryExpr.left); // NOTE: Recursively Visit each node.
            AST_PrintNode(node->Data->BinaryExpr.right);
            break;
    }
}

请注意,函数如何根据我们正在处理的节点类型来递归地访问每个节点。

让我们添加一个更复杂的示例,一个if语句构造!回想一下if语句还可以具有可选的else子句。让我们将if-else语句添加到我们的原始节点结构中。请记住,if语句本身也可以具有if语句,因此在节点系统中可能会发生某种递归。其他语句是可选的,因此该elsestmt字段可以为NULL,递归访问者函数可以忽略该字段。

struct ASTNode;
union SyntaxNode {
    int64_t         llVal;
    uint64_t        ullVal;
    struct {
        struct ASTNode *left, *right;
    } BinaryExpr;
    struct {
        struct ASTNode *expr, *stmt, *elsestmt;
    } IfStmt;
};

enum SyntaxNodeType {
    AST_IntVal, AST_Add, AST_Sub, AST_Mul, AST_Div, AST_Mod, AST_IfStmt, AST_ElseStmt, AST_Stmt
};

struct ASTNode {
    union SyntaxNode *Data;
    enum SyntaxNodeType Type;
};

回到称为的节点访问者打印函数中AST_PrintNode,我们可以if通过添加以下C代码来容纳语句AST构造:

case AST_IfStmt:
    puts("AST If Statement\n");
    AST_PrintNode(node->Data->IfStmt.expr);
    AST_PrintNode(node->Data->IfStmt.stmt);
    AST_PrintNode(node->Data->IfStmt.elsestmt);
    break;

就如此容易!总之,语法树只不过是带有标记的树及其数据本身的并集的树!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.