it-swarm.com.de

Wie genau wird ein abstrakter Syntaxbaum erstellt?

Ich glaube, ich verstehe das Ziel eines AST und habe schon einige Baumstrukturen gebaut, aber niemals einen AST. Ich bin größtenteils verwirrt, weil die Knoten aus Text und nicht aus Zahlen bestehen. Daher kann ich mir keine gute Möglichkeit vorstellen, ein Token/einen String einzugeben, wenn ich Code analysiere.

Wenn ich zum Beispiel Diagramme von ASTs betrachtete, waren die Variable und ihr Wert Blattknoten mit einem Gleichheitszeichen. Das macht für mich vollkommen Sinn, aber wie würde ich vorgehen, um dies umzusetzen? Ich denke, ich kann es von Fall zu Fall tun, so dass ich, wenn ich auf ein "=" stoße, dieses als Knoten verwende und den vor dem "=" analysierten Wert als Blatt hinzufüge. Es scheint einfach falsch zu sein, weil ich wahrscheinlich Fälle für Tonnen und Tonnen von Dingen machen müsste, abhängig von der Syntax.

Und dann bin ich auf ein anderes Problem gestoßen: Wie wird der Baum durchquert? Gehe ich den ganzen Weg die Höhe hinunter und gehe einen Knoten zurück, wenn ich den Boden erreicht habe, und mache dasselbe für seinen Nachbarn?

Ich habe Tonnen von Diagrammen auf ASTs gesehen, aber ich konnte kein ziemlich einfaches Beispiel für eines im Code finden, was wahrscheinlich helfen würde.

47
Howcan

Die kurze Antwort lautet, dass Sie Stapel verwenden. This ist ein gutes Beispiel, aber ich werde es auf einen AST anwenden.

Zu Ihrer Information, dies ist Edsger Dijkstras Shunting-Yard-Algorithmus .

In diesem Fall verwende ich einen Operatorstapel und einen Ausdrucksstapel. Da Zahlen in den meisten Sprachen als Ausdrücke betrachtet werden, verwende ich den Ausdrucksstapel, um sie zu speichern.

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()

(Bitte seien Sie nett zu meinem Code. Ich weiß, dass er nicht robust ist; er soll nur Pseudocode sein.)

Wie Sie dem Code entnehmen können, können beliebige Ausdrücke Operanden für andere Ausdrücke sein. Wenn Sie folgende Eingabe haben:

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

der Code, den ich geschrieben habe, würde diesen AST erzeugen:

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

Und wenn Sie dann den Code für diesen AST erstellen möchten, führen Sie einen Post Order Tree Traversal aus. Wenn Sie einen Blattknoten (mit einer Zahl) besuchen, generieren Sie eine Konstante, da der Compiler die Operandenwerte kennen muss. Wenn Sie einen Knoten mit einem Operator besuchen, generieren Sie die entsprechende Anweisung vom Operator. Zum Beispiel gibt Ihnen der Operator '+' eine Anweisung zum Hinzufügen.

47
Gavin Howard

Es gibt einen signifikanten Unterschied zwischen der Darstellung eines AST] im Test (ein Baum mit Zahlen/Variablen an den Blattknoten und Symbolen an den inneren Knoten) und der tatsächlichen Implementierung.

Die typische Implementierung eines AST (in einer OO Sprache)) verwendet stark Polymorphismus. Die Knoten im AST werden normalerweise mit einer Vielzahl von Klassen implementiert, die alle von einer gemeinsamen ASTNode -Klasse abgeleitet sind. Für jedes syntaktische Konstrukt in der Sprache, die Sie verarbeiten, gibt es eine Klasse zur Darstellung dieses Konstrukts im AST, z. B. ConstantNode (für Konstanten wie 0x10 oder 42), VariableNode (für Variablennamen), AssignmentNode (für Zuweisungsoperationen), ExpressionNode (für generische Ausdrücke) usw.
Jeder bestimmte Knotentyp gibt an, ob dieser Knoten untergeordnete Knoten hat, wie viele und möglicherweise von welchem ​​Typ. Ein ConstantNode hat normalerweise keine Kinder, ein AssignmentNode hat zwei und ein ExpressionBlockNode kann eine beliebige Anzahl von Kindern haben.

Das AST wird vom Parser erstellt, der weiß, welches Konstrukt es gerade analysiert hat, damit es die richtige Art von AST Knoten) erstellen kann.

Beim Durchlaufen des AST kommt der Polymorphismus der Knoten wirklich ins Spiel. Die Basis ASTNode definiert die Operationen, die an den Knoten ausgeführt werden können, und jeder bestimmte Knotentyp implementiert diese Operationen auf die spezifische Weise für dieses bestimmte Sprachkonstrukt.

Das Erstellen des AST aus dem Quelltext ist "einfach" Parsing . Wie genau dies durchgeführt wird, hängt von der analysierten formalen Sprache und der Implementierung ab. Sie können Parser-Generatoren wie verwenden menhir (für Ocaml) , GNU bison mit flex oder ANTLR etc etc. Es wird oft "manuell" gemacht, indem ein rekursiver Abstiegsparser (siehe diese Antwort erklärt warum) codiert wird. Der kontextbezogene Aspekt des Parsens ist oft anderswo gemacht (Symboltabellen, Attribute, ....).

In der Praxis sind AST jedoch viel komplexer als Sie glauben. Zum Beispiel in einem Compiler wie GCC the = AST enthält Informationen zum Quellspeicherort und einige Tippinformationen. Lesen Sie mehr über Generic Trees in GCC und schauen Sie in dessen gcc/tree.def . BTW, Schauen Sie auch nach innen GCC MELT (das ich entworfen und implementiert habe), es ist relevant für Ihre Frage.

Ich weiß, dass diese Frage 4+ Jahre alt ist, aber ich denke, ich sollte eine detailliertere Antwort hinzufügen.

Abstrakte Syntax Bäume werden nicht anders als andere Bäume erstellt. Die wahrere Aussage in diesem Fall ist, dass Syntaxbaumknoten je nach Bedarf eine unterschiedliche Anzahl von Knoten haben.

Ein Beispiel sind binäre Ausdrücke wie 1 + 2 Ein einfacher Ausdruck wie dieser würde einen einzelnen Wurzelknoten erzeugen, der einen rechten und einen linken Knoten enthält, der die Daten über die Zahlen enthält. In C-Sprache würde es ungefähr so ​​aussehen

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;
};

Ihre Frage war auch, wie man durchquert? Das Durchlaufen wird in diesem Fall Visiting Nodes genannt. Für den Besuch jedes Node müssen Sie jeden Knotentyp verwenden, um zu bestimmen, wie die Daten jedes Syntaxknotens ausgewertet werden sollen.

Hier ist ein weiteres Beispiel dafür in C, wo ich einfach den Inhalt jedes Knotens drucke:

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;
    }
}

Beachten Sie, wie die Funktion jeden Knoten rekursiv besucht, je nachdem, um welchen Knotentyp es sich handelt.

Fügen wir ein komplexeres Beispiel hinzu, ein if - Anweisungskonstrukt! Denken Sie daran, dass if-Anweisungen auch eine optionale else-Klausel enthalten können. Fügen wir die if-else-Anweisung zu unserer ursprünglichen Knotenstruktur hinzu. Denken Sie daran, dass if-Anweisungen selbst auch if-Anweisungen haben können, sodass eine Art Rekursion innerhalb unseres Knotensystems auftreten kann. Andernfalls sind Anweisungen optional, sodass das Feld elsestmt NULL sein kann, was die rekursive Besucherfunktion ignorieren kann.

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;
};

zurück in unserer Knotenbesucher-Druckfunktion namens AST_PrintNode können wir die Anweisung if AST Konstrukt) durch Hinzufügen dieses C-Codes aufnehmen:

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;

So einfach ist das! Zusammenfassend ist der Syntaxbaum nicht viel mehr als ein Baum einer markierten Vereinigung des Baums und seiner Daten selbst!

2
Nergal