私はASTの目標を理解していると思います。以前にいくつかのツリー構造を構築しましたが、ASTを構築したことはありません。ノードはテキストで番号ではないので、私はほとんど混乱しています。コードを解析しているときに、トークン/文字列を入力するための素晴らしい方法は考えられません。
たとえば、ASTの図を見ると、変数とその値は等号のリーフノードでした。これは完全に理にかなっていますが、これを実装するにはどうすればよいでしょうか。私はケースバイケースでそれを行うことができると思うので、「=」に出会ったとき、それをノードとして使用し、「=」の前に解析された値をリーフとして追加します。構文に応じて、何トンものケースを作成する必要があるため、それは間違っているように見えます。
そして、私は別の問題に遭遇しました、木はどのように横断されますか?私は高さをずっと下に行き、底にぶつかったときにノードを上に戻り、その隣人にも同じことをしますか?
ASTで大量の図を確認しましたが、コード内に非常に単純な例を見つけることができなかったため、おそらく役立つでしょう。
簡単に言えば、スタックを使用することです。 これ は良い例ですが、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()
(私のコードについては、Niceにしてください。堅牢ではないことがわかっています。これは、疑似コードであることになっています。)
とにかく、コードからわかるように、任意の式は他の式のオペランドになる可能性があります。次の入力がある場合:
5 * 3 + (4 + 2 % 2 * 8)
私が書いたコードは、このASTを生成します。
+
/ \
/ \
* +
/ \ / \
5 3 4 *
/ \
% 8
/ \
2 2
そして、そのASTのコードを生成する場合は、 Post Order Tree Traversal を実行します。リーフノード(番号付き)にアクセスすると、コンパイラはオペランドの値を知る必要があるため、定数を生成します。オペレーターがいるノードにアクセスすると、オペレーターから適切な指示が生成されます。たとえば、「+」演算子は「追加」命令を提供します。
ASTがテストで通常どのように表示されるか(リーフノードに数値/変数があり、内部ノードにシンボルがあるツリー))と実際の実装方法には、大きな違いがあります。
AST(OO言語で)の典型的な実装は、多態性を多用します。ASTは通常、さまざまなクラスで実装され、すべてが共通のASTNode
クラスから派生します。処理している言語の構文構文ごとに、ConstantNode
などのASTでその構文を表すためのクラスがあります(定数などの定数の場合) 0x10
または42
)、VariableNode
(変数名の場合)、AssignmentNode
(代入演算の場合)、ExpressionNode
(一般式の場合)など.
各特定のノードタイプは、そのノードに子があるかどうか、その数、および場合によってはどのタイプかを指定します。通常、ConstantNode
には子がなく、AssignmentNode
には2つあり、ExpressionBlockNode
には任意の数の子を含めることができます。
ASTは、構文解析したばかりの構文を知っているパーサーによって作成されるため、適切な種類のASTノードを構築できます。
ASTをトラバースするとき、ノードのポリモーフィズムが実際に機能します。ベースASTNode
は、ノードで実行できる操作を定義し、特定のノードタイプごとに、その特定の言語構成に特定の方法でこれらの操作を実装します。
ソーステキストからASTを構築することは「単純に」 parsing です。どの程度正確に行われるかは、解析された形式によって異なります言語と実装。 menhir(Ocamlの場合) 、GNU bison
with flex
、または [〜#〜] antlr [〜#〜] など。多くの場合、いくつかをコーディングすることで「手動」で行われます 再帰降下パーサー ( この回答 の説明を参照してください。)構文解析のコンテキストは、他の場所(シンボルテーブル、属性、...)。
ただし、実際にはASTは信じているよりもはるかに複雑です。たとえば、 [〜#〜] gcc [〜#〜]のようなコンパイラでは ASTはソースの場所情報といくつかの入力情報を保持します。 Generic Trees GCCでその gcc/tree.def の内部を調べます。ところで、 GCC MELT (私が設計して実装しました)、それはあなたの質問に関連しています。
この質問は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と呼ばれます。各Nodeにアクセスするには、各ノードタイプを使用して、各構文ノードのデータを評価する方法を決定する必要があります。
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ステートメントを持つことができるので、ノードシステム内で一種の再帰が発生する可能性があることに注意してください。 Elseステートメントはオプションであるため、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
というノードビジタープリント関数に戻ると、次のCコードを追加することで、if
ステートメント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;
とても簡単です!結論として、構文ツリーは、ツリーとそのデータ自体のタグ付きユニオンのツリーにすぎません。