私はかなり古典的なアーキテクチャを考案したコンパイラを書いています。それは、レクサーとパーサーから始まり、マクロプロセッサ、続いてセマンティック分析/型チェッカーパス、最後に中間でパイプ処理された順次パスで構成されていますコードジェネレーター(およびおそらく後で来るIRオプティマイザー)。
私の現在のアプローチは次のとおりです。パーサーはASTを構築しており、各タイプのASTノードは "AST"基本クラスから継承します。ASTの仮想関数が実装される予定です。後続のパスの機能簡単な例を提供するには:
macroExpand()
は、AST内のすべてのマクロを再帰的に検索、評価、および置換します。typeCheck()
は、型チェック、型推論、および一般的なセマンティック分析/エラーチェックを、現在は非糖化されたツリーで実行し、各ノードに型アノテーション(メンバー変数として実装)を完成させます。codeGen()
は、最後に、注釈付きASTからある種のIRを生成します。ただし、このすべての機能を単一のクラスに含めることは、単一責任の原則に違反することを恐れています。
マクロ展開は特に適合しないと思います。セマンティック分析とコード生成を、ツリー全体を2回トラバースする必要がないと思うという理由だけで、それらを分離するのではなく、1つの関数セットに統合することを考えていました。以前にそれらを事前に推測して事前にチェックしたとしても、とにかくコード生成時に型を調べる必要があります。
しかし、この構造上の変更があっても、少なくとも2つの完全に異なるメソッドのセットが私のAST=クラスにあります。これ自体が私の中で悪いことになる特定の理由はまだわかりません特定のケースですが、単一の責任原則が正当な理由で発見されたと確信しています。
この問題を解決する1つの方法(?)はビジターパターンを使用し、ASTの目的ごとに個別のビジタークラス階層を記述します(マクロ展開、セマンティック分析)。および/またはコード生成)しかし、私は本当にそうするのが好きではありません(正直に言って、私はアイデアを本当に嫌いです)。クラス階層)。
現在、私はこのコンパイラをC++で書いていますが、事後の変更とクラスの拡張を許可する言語(Objective-Cカテゴリなど)を使用している場合は、きっと利用するでしょう。言語のこの機能と私は、「コア」インターフェイスやクラスの実装とは関係なく、ベースASTクラスを必要なメソッドのセットで装飾するだけです。
C++でそれをシミュレートすることができますすべてのメソッド宣言を1つのヘッダーファイルに入れますが、関数の各カテゴリの実装を異なる実装ファイルに記述します。ただし、これは通常の「1つのクラス」と矛盾します。 、1つのファイル」の練習。
要約すると、私の質問は次のとおりです。1つのクラスに2つまたは3つの異なる機能を与えるという私の現在のアプローチは本当に悪いですか?
もしそうなら、私の提案された修正のいずれかが良い習慣であると考えられていますか、または…
そうでない場合は、もっと良いものを提案できますか?
はい、それはひどい、ひどい計画です。それらは完全に異なるものです。 SRPに違反することは、問題の始まりにすぎません。他の問題は、たとえば、ツールの解析のみのサポートを提供できないなどです。
セマンティックアナライザーは、ASTからセマンティックグラフへの変換を実行する必要があります。ASTを変更したり、セマンティック情報を保持したりしないでください。 ASTは、コード生成情報を保持してはなりません。ASTは、構文を保持する以外のことをしてはなりません。
これらのツリーとグラフはすべて、まったく異なる表現と実装でまったく異なることを行うため、並列クラス階層の問題は実際には発生しません。セマンティックツリーが構文ツリーと密接に対応している場合は、おそらく非常に基本的なインタープリターを構築しているか、それを非常に間違っています。