web-dev-qa-db-fra.com

Comment un arbre de syntaxe abstrait est-il créé exactement?

Je pense que je comprends le but d'un AST, et j'ai déjà construit quelques structures arborescentes, mais jamais un AST. Je suis surtout confus parce que les nœuds sont du texte et non du nombre, donc je ne peux pas penser à une belle façon d'entrer un jeton/chaîne alors que j'analyse du code.

Par exemple, lorsque j'ai regardé des diagrammes d'AST, la variable et sa valeur étaient des nœuds feuilles d'un signe égal. Cela est parfaitement logique pour moi, mais comment pourrais-je procéder pour l'implémenter? Je suppose que je peux le faire au cas par cas, de sorte que lorsque je tombe sur un "=" je l'utilise comme nœud et ajoute la valeur analysée avant le "=" comme feuille. Cela semble juste faux, car je devrais probablement faire des cas pour des tonnes et des tonnes de choses, selon la syntaxe.

Et puis je suis tombé sur un autre problème, comment l'arbre est-il traversé? Dois-je descendre tout le long de la hauteur et remonter un nœud lorsque je touche le bas et faire de même pour son voisin?

J'ai vu des tonnes de diagrammes sur les AST, mais je n'ai pas pu trouver un exemple assez simple d'un dans le code, ce qui serait probablement utile.

47
Howcan

La réponse courte est que vous utilisez des piles. This est un bon exemple, mais je l'appliquerai à un AST.

Pour info, il s'agit de Shunting-Yard Algorithm d'Edsger Dijkstra.

Dans ce cas, j'utiliserai une pile d'opérateurs et une pile d'expressions. Étant donné que les nombres sont considérés comme des expressions dans la plupart des langues, je vais utiliser la pile d'expressions pour les stocker.

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

(Soyez gentil avec mon code. Je sais qu'il n'est pas robuste; il est juste censé être un pseudocode.)

Quoi qu'il en soit, comme vous pouvez le voir dans le code, les expressions arbitraires peuvent être des opérandes vers d'autres expressions. Si vous avez l'entrée suivante:

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

le code que j'ai écrit produirait cette AST:

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

Et puis quand vous voulez produire le code pour cet AST, vous faites un Post Order Tree Traversal . Lorsque vous visitez un nœud feuille (avec un nombre), vous générez une constante car le compilateur doit connaître les valeurs de l'opérande. Lorsque vous visitez un nœud avec un opérateur, vous générez l'instruction appropriée à partir de l'opérateur. Par exemple, l'opérateur "+" vous donne une instruction "ajouter".

47
Gavin Howard

Il y a une différence significative entre la façon dont un AST est généralement représenté dans le test (un arbre avec des nombres/variables aux nœuds feuilles et des symboles aux nœuds intérieurs) et comment il est réellement mis en œuvre.

L'implémentation typique d'un AST (dans un OO langage) fait un usage intensif du polymorphisme. Les nœuds dans le AST sont généralement implémentées avec une variété de classes, toutes dérivant d'une classe ASTNode commune. Pour chaque construction syntaxique dans le langage que vous traitez, il y aura une classe pour représenter cette construction dans l'AST, telle que ConstantNode (pour les constantes, telles que 0x10 ou 42), VariableNode (pour les noms de variables), AssignmentNode (pour les opérations d'affectation), ExpressionNode (pour les expressions génériques), etc.
Chaque type de nœud spécifique spécifie si ce nœud a des enfants, combien et éventuellement de quel type. Un ConstantNode n'aura généralement pas d'enfant, un AssignmentNode en aura deux et un ExpressionBlockNode pourra avoir n'importe quel nombre d'enfants.

Le AST est construit par l'analyseur, qui sait quelle construction il vient d'analyser, afin qu'il puisse construire le bon type de AST Node.

Lors de la traversée de l'AST, le polymorphisme des nœuds entre vraiment en jeu. La base ASTNode définit les opérations qui peuvent être effectuées sur les nœuds, et chaque type de nœud spécifique implémente ces opérations de la manière spécifique pour cette construction de langage particulière.

18

Construire le AST à partir du texte source est "simplement" analyse . La façon exacte dont cela se fait dépend du langage formel analysé et de l'implémentation. Vous pouvez utiliser des générateurs d'analyseurs comme menhir (pour Ocaml) , GNU bison avec flex, ou ANTLR etc etc. Cela se fait souvent "manuellement" en codant --- analyseur de descente récursif (voir cette réponse expliquant pourquoi). L'aspect contextuel de l'analyse est souvent fait ailleurs (tables de symboles, attributs, ....).

Cependant, en pratique AST sont beaucoup plus complexes que ce que vous croyez. Par exemple, dans un compilateur comme GCC the = AST conserve les informations de localisation de la source et certaines informations de frappe. Lisez à propos de Generic Trees dans GCC et regardez à l'intérieur de son gcc/tree.def . BTW, regardez aussi à l'intérieur GCC MELT (que j'ai conçu et implémenté), il est pertinent pour votre question.

9

Je sais que cette question a plus de 4 ans mais je pense que je devrais ajouter une réponse plus détaillée.

Les arbres de syntaxe abstraite ne sont pas créés différemment des autres arbres; la déclaration la plus vraie dans ce cas est que les nœuds de l'arbre de syntaxe ont une quantité variée de nœuds AU BESOIN.

Un exemple est les expressions binaires comme 1 + 2 Une expression simple comme celle-ci créerait un nœud racine unique contenant un nœud droit et gauche contenant les données sur les nombres. En langage C, cela ressemblerait à quelque chose comme

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

Votre question était aussi comment traverser? Dans ce cas, la traversée est appelée Noeuds visiteurs . La visite de chaque Node nécessite que vous utilisiez chaque type de nœud pour déterminer comment évaluer les données de chaque nœud de syntaxe.

Voici un autre exemple de cela en C où j'imprime simplement le contenu de chaque nœud:

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

Remarquez comment la fonction visite récursivement chaque nœud en fonction du type de nœud auquel nous avons affaire.

Ajoutons un exemple plus complexe, une construction d'instruction if! Rappelez-vous que les instructions if peuvent également avoir une clause else facultative. Ajoutons l'instruction if-else à notre structure de nœuds d'origine. N'oubliez pas que les instructions if elles-mêmes peuvent également avoir des instructions if, donc une sorte de récursivité au sein de notre système de nœuds peut se produire. Les autres instructions sont facultatives, donc le champ elsestmt peut être NULL que la fonction visiteur récursive peut ignorer.

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

de retour dans notre fonction d'impression de visiteur de noeud appelée AST_PrintNode, nous pouvons accueillir l'instruction if AST construct en ajoutant ce code C:

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;

Aussi simple que cela! En conclusion, l'arbre de syntaxe n'est pas beaucoup plus qu'un arbre d'une union étiquetée de l'arbre et de ses données elles-mêmes!

2
Nergal