抽象構文ツリーはどのくらい正確に作成されますか?


47

私はASTの目標を理解しており、以前にツリー構造をいくつか構築したことがありますが、ASTを構築したことはないと思います。ノードはテキストであり番号ではないため、ほとんど混乱しています。そのため、コードを解析しているときにトークン/文字列を入力する良い方法は考えられません。

たとえば、ASTの図を見ると、変数とその値は等号のリーフノードでした。これは私にとって完全に理にかなっていますが、これをどのように実装しますか?私はケースバイケースでそれを行うことができると思うので、「=」につまずいたとき、ノードとしてそれを使用し、「=」の前に解析された値をリーフとして追加します。私はおそらく構文に応じて、たくさんの事柄のケースを作成しなければならないので、それは間違っているようです。

そして、私は別の問題に遭遇しました、ツリーはどのように横断されますか?高さを一番下に移動し、最下部に到達したらノードを上に移動し、隣のノードでも同じことをしますか?

ASTで多数の図を見てきましたが、コードで簡単な例を見つけることができませんでした。おそらく役立つでしょう。


不足している重要な概念は再帰です。再帰は一種の直観に反するものであり、最終的に「クリック」する場合は学習者ごとに異なりますが、再帰がなければ、構文解析(および他の多くの計算トピックも)を理解する方法はありません。
キリアンフォス14

私は再帰を取得しました。この場合、それを実装するのは難しいと思いました。私は実際に再帰を使用したかったので、一般的な解決策では機能しない多くのケースになりました。Gdhowardの答えは、今私を大いに助けています。
ハウカン14

演習としてRPN計算機を構築することは、演習になる可能性があります。それはあなたの質問に答えませんが、いくつかの必要なスキルを教えるかもしれません。

実際にRPN計算機を作成したことがあります。答えは私を大いに助け、私は今基本的なASTを作ることができると思う。ありがとう!
ハウカン14

回答:


47

簡単な答えは、スタックを使用することです。これは良い例ですが、ASTに適用します。

参考までに、これはEdsger DijkstraのShunting-Yard Algorithmです。

この場合、演算子スタックと式スタックを使用します。数値はほとんどの言語で式と見なされるため、式スタックを使用して数値を保存します。

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、右から左への演算子の機能を追加するのは非常に簡単です。最も簡単なのは、ルックアップテーブルを追加し、演算子が右から左の場合、オペランドの順序を単に逆にすることです。
ギャビンハワード

4
@Simon両方をサポートしたい場合は、シャンティングヤードアルゴリズムを最大限に活用することをお勧めします。アルゴリズムが進むにつれて、それは絶対的なクラッカーです。
-biziclop

19

テストでのASTの典型的な表示方法(リーフノードに数字/変数、内部ノードにシンボルを持つツリー)と実際に実装される方法には大きな違いがあります。

(OO言語での)ASTの典型的な実装では、ポリモーフィズムが多用されます。ASTのノードは通常、さまざまなクラスで実装され、すべてが共通のASTNodeクラスから派生しています。処理している言語の各構文構成要素について、(またはConstantNodeなどの定数の0x10場合42)、VariableNode(変数名の場合)、AssignmentNode(割り当て操作の場合)、ExpressionNode(汎用の式)など。
特定のノードタイプごとに、そのノードに子があるかどうか、いくつあるか、場合によってはどのタイプかを指定します。A ConstantNodeは通常、子を持たず、AssignmentNode2つをExpressionBlockNode持ち、任意の数の子を持つことができます。

ASTはパーサーによって構築されます。パーサーは、構文解析したばかりのコンストラクトを知っているため、適切な種類のASTノードを構築できます。

ASTを通過するとき、ノードのポリモーフィズムが実際に作用します。ベースASTNodeは、ノードで実行できる操作を定義し、特定の各ノードタイプは、特定の言語構成に対して特定の方法でそれらの操作を実装します。


9

ソーステキストからASTを構築することは、「単純に」解析することです。どのように正確に行われるかは、解析された形式言語と実装に依存します。menhir(Ocamlの場合)、GNU bisonwith flexANTLRなどのようなパーサージェネレーターを使用できます。多くの場合、再帰降下パーサーをコーディングすることで「手動で」実行されます(理由を説明するこの回答を参照)。構文解析のコンテキストの側面は、他の場所(シンボルテーブル、属性など)で行われることがよくあります。

ただし、実際にはASTはあなたが信じているよりもはるかに複雑です。たとえば、GCCのようなコンパイラでは、ASTはソースの位置情報と一部の入力情報を保持します。GCCのGeneric Treesについて読んで、そのgcc / tree.defを調べてください。ところで、GCC MELT(私が設計および実装した)の内部も見てください。これはあなたの質問に関連しています。


ソーステキストを解析し、JSの配列に変換するLuaインタープリターを作成しています。ASTと見なすことはできますか?私はこのようなことをすることになっています: --My comment #1 print("Hello, ".."world.") `[{" type ":"-"、" content ":" My comment#1 "}、{" type ":" call "、" name ":"に変換しますprint "、" arguments ":[[{" type ":" str "、" action ":" .. "、" content ":" Hello、 "}、{" type ":" str "、" content ": "世界。" }]]}] `JSでは他のどの言語よりもずっとシンプルだと思います!
Hydroper

@TheProHandsこれは、ASTではなくトークンと見なされます。
YoYoYonnY

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

あなたの質問はどのように横断するのですか?この場合の走査は、Visiting Nodesと呼ばれます。各ノードにアクセスするには、各ノードタイプを使用して、各構文ノードのデータを評価する方法を決定する必要があります。

次に、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;
};

呼び出されたノードビジターの印刷関数に戻り、次のCコードを追加することでAST_PrintNodeifステートメント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;

それと同じくらい簡単!結論として、構文ツリーは、ツリーとそのデータ自体のタグ付き結合のツリーにすぎません!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.