9

Updateは、正しい方向を示したため、Ira Baxterの回答を受け入れました。コンパイル段階の実装を開始することで、実際に必要なものを最初に理解しました。ノード内をトラバースすると、不可能なアプローチになることがすぐに明らかになりました。すべてのノードにアクセスする必要はなく、一部のノードには逆の順序でアクセスする必要があります(たとえば、最初に割り当てのrhsを実行して、コンパイラーがタイプがrhs / operatorと一致するかどうかを確認できるようにします)。訪問者にトラバーサルを配置すると、これはすべて非常に簡単になります。

アプリケーションで使用されるミニ言語の処理の主要なリファクトリーを決定する前に、ASTなどで遊んでいます。レクサー/パーサーを作成しましたが、ASTを問題なく取得できます。ビジターもあり、具体的な実装として、元のソースファイルを再作成するだけのASTToOriginalを作成しました。最終的には、Vsisitorも実装し、実行時に実際のC ++コードを作成するある種のコンパイラーが登場するので、最初からすべてが正しいことを確認したいと思います。現在はすべて正常に機能していますが、トラバーサル順序はビジター自体に実装されているため、類似した/重複したコードがいくつかあります。

より多くの情報を調べるとき、いくつかの実装は、各具体的な訪問者でこれを繰り返さないために、代わりに訪問されたオブジェクト自体でトラバーサル順序を維持することを好むようです。同じように、GoFでさえこれについて簡単に話します。だから私もこのアプローチを試してみたかったのですが、すぐに行き詰まりました。説明させてください。

サンプルソースラインと対応するASTノード:

if(t>100?x=1;sety(20,true):x=2)
Conditional
  BinaryOp
    left=Variable [name=t], operator=[>], right=Integer [value=100]
  IfTrue
    Assignment
      left=Variable [name=x], operator=[=], right=Integer [value=1] 
    Method
      MethodName [name=sety], Arguments( Integer [value=20], Boolean [value=true] )
  IfFalse
    Assignment
      left=Variable [name=x], operator=[=], right=Integer [value=1]

いくつかのコード:

class BinaryOp {
  void Accept( Visitor* v ){ v->Visit( this ); }
  Expr* left;
  Op* op;
  Expr* right;
};    
class Variable {
  void Accept( Visitor* v ){ v->Visit( this ); }
  Name* name;
};
class Visitor { //provide basic traversal, terminal visitors are abstract
  void Visit( Variable* ) = 0;
  void Visit( BinaryOp* p ) {
    p->left->Accept( this );
    p->op->Accept( this );
    p->right->Accept( this );        
  }
  void Visit( Conditional* p ) {
    p->cond->Accept( this );
    VisitList( p->ifTrue ); //VisitList just iterates over the array, calling Accept on each element
    VisitList( p->ifFalse );
  }
};

ASTToOriginalの実装は非常に簡単です。すべての抽象Visitorメソッドは、ターミナルの名前または値のメンバーを出力するだけです。非終端記号の場合、それは依存します。条件付きの追加コードが必要なため、割り当ての印刷はデフォルトのビジタートラバーサルで問題なく機能します。

class ASTToOriginal {
  void Visit( Conditional* p ) {
    str << "if(";
    p->cond->Accept( this );
    str << "?";
      //VisitListWithPostOp is like VisitList but calls op for each *except the last* iteration
    VisitListWithPostOp( p->ifTrue, AppendText( str, ";" ) );
    VisitListWithPostOp( p->ifFalse, AppendText( str, ";" ) );
    str << ")";
  }
};

したがって、VisitorのConditionalのVisitメソッドとASTToOriginalの両方が実際に非常に似ていることがわかります。ただし、ノードにトラバーサルを配置してこれを解決しようとすると、事態が悪化するだけでなく、完全に混乱します。PreVisitメソッドとPostVisitメソッドを使用していくつかの問題を解決するアプローチを試しましたが、ノードにコードを追加するだけでした。また、閉じ括弧などをいつ追加するかを知るために、訪問者内のいくつかの状態を追跡する必要があるように見え始めました。

class BinaryOp {
  void Accept( Conditional* v ) { 
    v->Visit( this );
    op->Accept( v )
    VisitList( ifTrue, v );
    VisitList( ifFalse, v );
};
class Vistor {
  //now all methods are pure virtual
};
class ASTToOriginal {
  void Visit( Conditional* p ) {
    str << "if(";
    //now what??? after returning here, BinaryOp will visit the op automatically so I can't insert the "?"
    //If I make a PostVisit( BinaryOp* ), and call it it BinaryOp::Accept, I get the chance to insert the "?",
    //but now I have to keep a state: my PostVisit method needs to know it's currently being called as part of a Conditional
    //Things are even worse for the ifTrue/ifFalse statement arrays: each element needs a ";" appended, but not the last one,
    //how am I ever going to do that in a clean way?
  }
};

質問:このアプローチは私の場合には適していないのですか、それとも私は何か重要なものを見落としていますか?これらの問題に対処するための共通の設計はありますか?別の方向へのトラバーサルも必要な場合はどうなりますか?

4

3 に答える 3

6

2つの問題があります:

  • どの子ノードにアクセスできますか?
  • あなたは彼らをどのような順序で訪問すべきですか?

おそらく、実際の子ノードはノードタイプによって認識されている必要があります。実際には、それは文法によって知られ、文法から一般の訪問者に「反映」されるべきです。

ノードにアクセスする順序は、何をする必要があるかによって完全に異なります。プリティプリントを実行している場合は、左から右への子の順序が理にかなっています(子ノードが文法の順序でリストされている場合は、そうではない可能性があります)。シンボルテーブルを作成している場合は、ステートメント本体の子にアクセスする前に、宣言の子にアクセスする必要があります。

さらに、どの情報がツリーの上下に流れるかについて心配する必要があります。変数のリストへのアクセスはツリーを上に流れます。構築されたシンボルテーブルは、宣言からツリーを上に流れ、ステートメント本体の子に戻ります。そして、この情報の流れは訪問順序を強制します。ステートメント本体に渡すシンボルテーブルを作成するには、最初にシンボルテーブルを作成し、宣言の子から渡す必要があります。

私はこれらの問題があなたにgreifを与えているものだと思います。実際には訪問順序が完全にタスクに依存し、ツリーで実行する可能性のあるさまざまなタスクが多数あり、それぞれが独自の情報フローを持ち、したがって順序に依存している場合、訪問者に単一の構造を課そうとしています。

これを解決する方法の1つは、attribute(d)grammar(AG)の概念を使用することです。この概念では、さまざまなタイプの属性とそれらの計算/使用方法で文法規則を装飾します。たとえば、文法規則の注釈として文字通り計算を記述します。

  method = declarations statements ;
  <<ResolveSymbols>>: { declarations.parentsymbols=method.symboltable;
                        statements.symboltable = declarations.symboltable;
                      }

文法規則は、必要なノードタイプを示します。属性計算は、ツリーの下(method.symboltableへの参照は親から来るものへの参照)、ツリーの上(declarations.symbolテーブルへの参照はその子によって計算されたものへの参照)、またはツリー(statements.symboltableは、declarations.symboltableによって計算された値から、ステートメントの子に渡されます)。属性の計算により、訪問者が定義されます。実行される計算は「属性評価」と呼ばれます。

この特定の属性文法のこの表記は、DMS SoftwareReengineeringToolkitの一部です。他のAGツールも同様の表記法を使用しています。すべての(AG)スキームと同様に、特定のルールは、特定のノード( "メソッド")の目的固有( "ResolveSymbols")ビジターを作成するために使用されます。ノードごとにこのような仕様のセットを使用すると、実行可能な目的固有の訪問者のセットを取得できます。AGスキームの価値は、これを簡単に記述でき、すべての定型文が生成されることです。

このように抽象的に問題を考え、それから、これまでと同じように、目的別の訪問者を手作業で生成することができます。

于 2011-02-25T18:09:28.283 に答える
1

ツリーの再帰的な一般的なトラバースでは、VisitorとCompositeは通常、(最初に関連するgoogleリンク)のように一緒に使用されます。私は最初にこのアイデアについてそこで読みました。素晴らしいアイデアであるビジターコンビネーターもあります。

ところで...

これは、代数的データ型とパターンマッチングを備えた関数型言語が優れているところです。可能であれば、関数型言語に切り替えてください。CompositeとVisitorは、それぞれADTとパターンマッチングの言語サポートがないための醜い回避策にすぎません。

于 2011-02-25T17:56:21.820 に答える
0

IMO、各具象クラス(BinaryOp、Variableなど)でVisitorクラスを拡張します。このように、BinaryOpオブジェクトを作成するために必要なすべてのロジックは、BinaryOpクラスに存在します。このアプローチは、ウォークアバウトパターンに似ています。それはあなたの仕事をより簡単にするかもしれません。

于 2011-02-25T17:20:01.417 に答える