私はCで単純なおもちゃの言語用のコンパイラーを実装しています。スキャナーとパーサーが機能しており、ASTの概念的な機能/構築に関する合理的な背景があります。私の質問は、CでASTを表す特定の方法に関連しています。オンラインのさまざまなテキスト/リソースで、3つのスタイルに頻繁に出くわしました。
ノードのタイプごとに1つの構造体。
これには、すべての子構造体の最初のフィールドであるベースノード「クラス」(構造体)があります。ベースノードには、ノードのタイプ(定数、二項演算子、代入など)を格納する列挙型が含まれています。構造体のメンバーは、構造体ごとに1つのセットを持つマクロのセットを使用してアクセスされます。これは次のようになります。
struct ast_node_base {
enum {CONSTANT, ADD, SUB, ASSIGNMENT} class;
};
struct ast_node_constant {
struct ast_node_base *base;
int value;
};
struct ast_node_add {
struct ast_node_base *base;
struct ast_node_base *left;
struct ast_node_base *right;
};
struct ast_node_assign {
struct ast_node_base *base;
struct ast_node_base *left;
struct ast_node_base *right;
};
#define CLASS(node) ((ast_node_base*)node)->class;
#define ADD_LEFT(node) ((ast_node_add*)node)->left;
#define ADD_RIGHT(node) ((ast_node_add*)node)->right;
#define ASSIGN_LEFT(node) ((ast_node_assign*)node)->left;
#define ASSIGN_RIGHT(node) ((ast_node_assign*)node)->right;
ノードのレイアウトごとに1つの構造体。
これは上記のレイアウトとほとんど同じように見えますが、ast_node_addとast_node_assignの代わりに、両方を表すast_node_binaryがあります。これは、2つの構造体のレイアウトが同じであり、base-> classの内容のみが異なるためです。 。これの利点は、より均一なマクロのセット(1つのマクロのペアではなく左右のすべてのノードのLEFT(node))のようですが、欠点は、C型チェックがそれほど有用ではないようです。 (たとえば、ast_node_addのみが存在するはずのast_node_assignを検出する方法はありません)。
さまざまなタイプのノードデータを保持するためのユニオンを含む、合計1つの構造体。
私が与えることができるよりもこれのより良い説明は見つけることができます ここ 。前の例のタイプを使用すると、次のようになります。
struct ast_node {
enum { CONSTANT, ADD, SUB, ASSIGNMENT } class;
union { int value;
struct { struct ast_node* left;
struct ast_node* right; } op;
};
3番目のオプションは、再帰的トラバーサルがはるかに簡単になるため(ユニオンを優先して多くのポインターキャストが回避されるため)、最も好きになる傾向がありますが、C型チェックも利用しません。最初のオプションは、任意のノードのメンバーにアクセスするためにキャストされている構造体へのポインターに依存しているという点で最も危険なようです(同じノードの異なるメンバーでも、アクセスに異なるケースが必要です(ベースと左))が、これらのキャストはタイプですそれが議論の余地があるかもしれないようにチェックしました。私にとって2番目の選択肢は、両方の世界で最悪のように思えますが、何かが足りないかもしれません。
これらの3つのスキームのうち、どれが最適で、なぜですか?私がまだ出会っていないより良い4番目のオプションはありますか?私はそれらのどれも「万能」ソリューションではないと思いますので、それが重要な場合は私が実装している言語は静的に型付けされた命令型言語であり、Cのほとんど小さなサブセットです。
3番目(ユニオン)のレイアウトについて私が持っている特定の質問。 値フィールドのみを使用する場合、opが書き込まれる可能性に対応するために、値の後に空のスペースがありますか?
あなたはこれらの仕事のどれでもすることができます。
すべてのノードが「同じ」レイアウトになるため、私はユニオンレイアウトを好みます。
[左寄りまたは右寄りのリストではなく、「子サブリスト」オプション、たとえば、任意に大きく動的な子の配列があると便利な場合があります。]
この問題がコンパイラの構築を困難にする問題ではないことに気付くでしょう。むしろ、シンボルテーブルを持ち、さまざまな種類の分析を実行し、マシンレベルのIRを選択し、コードジェネレーターを構築し、コードの最適化を行います。次に、実際のユーザーに遭遇し、実際に何が間違っていたかを発見します:-}
他の問題に近づく機会があるように、私は1つを選んでそれを実行します。
Ira Baxterは、シンプルで前向きなものを提供してくれました answer 特に注目すべきは、今後遭遇する問題なので、この質問に焦点を当てます。
私がまだ出くわしていないより良い4番目のオプションはありますか?
命令型言語を使用してコンパイラーを作成していて、ASTのノードの概念のデータ構造を設計する際に問題が発生しています。 ML、OCaml、Haskell、F#などの関数型言語の世界では、 タグ付き共用体 を使用して、さまざまなノードタイプをすべて1つのデータ構造に保持します。これは基本的に作成したものです。
OPがこの問題のために関数型言語に切り替わるとは思いませんが、他の人が定期的にツリーを扱っている場合、関数型言語を学び、ツリーに関連する問題に使用する価値があると感じるかもしれません。