¬<><∪∪は任意の曖昧でない文脈自由文法(CFG)を扱うことができますが、文法が曖昧かどうかは、パーサを生成する時点では完全にチェックできません。現在の¬<><∪∪処理系は、¬<><∪∪ソースが曖昧である可能性がある場合、以下で説明されるような警告メッセージを出力します。警告メッセージが出力されなかった場合、文法が曖昧でないことが、¬<><∪∪処理系によって保証されます(バグが無い限り)。警告メッセージが出力された場合、¬<><∪∪ソースは曖昧であるかもしれませんし、そうでないかもしれません。この場合、文法が曖昧でないことは、ユーザによって保証されなければなりません。
警告メッセージが出力される文法が曖昧でないことを保証するのに、次のようなアプローチがあります。
$embed
を用いて、還元(後述)を回避します$parsable
にしてコンパイルを行い、曖昧でないことを確認しておきます現在の¬<><∪∪処理系は、まず LALR(1) のコンフリクトが存在するかどうかを調べます。続いて、簡単な追跡調査を行い、それらのコンフリクトから曖昧性をもたらさないものを除外し、残ったコンフリクトの一覧を警告メッセージとして出力します。以下に警告メッセージの例を示します。
曖昧な文法の例:
警告メッセージ:
LALR(1) は、¬<><∪∪の文脈では、入力されたトークンをスタックに積んでいき、可能ならばスタックのトップからいくつかを取り除き、取り除かれたものを子とするツリーに置き換えることを繰り返す、構文解析アルゴリズムです。最後にスタックに1つだけ残ったツリーが、構文木になります(¬<><∪∪は最初に具象構文木を作成するため、別名もノードを作ります)。ノードの置き換えを還元と呼び、入力されたトークンをスタックに積むことをシフトと呼びます。LALR(1) では、次に入力されるトークン1つを見て、そのトークンをシフトするか、そのトークンは置いておいて今あるスタックに対して還元操作を行うかを決定しなければなりません。決定できないことを、コンフリクトと呼びます。コンフリクトには、異なる2つ以上の還元が可能な場合(還元/還元コンフリクト)と、還元もシフトも可能な場合(シフト/還元コンフリクト)があります。上の例では、次のシフト/還元コンフリクトが報告されています。
Addition
が期待される状況で、expression
"+"
expression
が入力された(つまりスタックにつまれた)状態で、
"+"
だった場合に、
expression
"+"
expression
を Addition
に還元できるexpression
が続き、スタックが expression
"+"
expression
"+"
expression
になった時点で、トップの3つを Addition
に還元することができる)LALR(1) は、異なる状態を同一視することで、リソースの消費を抑えます。コンフリクトは、同一視される状態に対して1回だけ報告されるため、上で報告されている入力は、そのコンフリクトの一例であることに注意してください。また、その入力に対しては行えないはずの還元やシフトが可能であると報告されることもあります。詳細は、構文解析に関する文献を参照してください。
次の方法があります。
AspectJ 1.0.4 を用いることで、¬<><∪∪が出力するJavaソースを直接書き換えたかのように、メソッドやフィールドを追加することができます。
ユーザにとっては負担のない理想的な方法ですが、提供者の負担が大きくなります。¬<><∪∪ソースや、¬<><∪∪処理系を変更した場合に、コードの書き換えを再び行わなければなりません。
¬<><∪∪が出力したクラス (OriginalParser
) を extends
した新しいクラスを作成し、そのインスタンスを、ノードを作成するファクトリーメソッドである createNode
で返します。
public class ExtendedParser extends OriginalParser { public interface Expression extends OriginalParser.Expression { public double evaluate(); // 追加されたメソッド:式の値を計算する } private class AdditionReplacement extends implements Expression OriginalParser.Default.Addition { ... public double evaluate() { Expression x1 = (Expression) operand1(); // operand1 は、OriginalParser.Expression なのでキャストが必要 Expression x2 = (Expression) operand2(); return x1.evaluate() + x2.evaluate(); } } ... protected Node createNode(int symbolID, NodeInitializationParameters parameters) { // Addition の代わりに AdditionReplacement を返す etc. ... } }
このようにして、ツリーのノードを、フィールドやメソッドが追加されたバージョンに差し替えます。この方法の欠点は、(ExtendedParser.Expression) operand1()
のようなキャストをユーザも行う必要があることと、多くのコードを手作業で追加しなければならないことです。
double evaluate(Expression expression) { if (expression instanceof Addition) { Addition addition = (Addition) expression; Expression x1 = addition.operand1(); Expression x2 = addition.operand2(); return evaluate(x1) + evaluate(x2); } ... }
Expression
に evaluate メソッドを追加する代わりに、Expression
を引数にとる evaluate メソッドを用意します。この方法の欠点は、Expression
を直接 extends
する新しいクラスを追加した場合は、必ず evaluate
を更新しなければならないこと(そして、更新を忘れてもコンパイルエラーが発生しないこと)です。また、本来の目標であるノードに対するフィールドやメソッドの追加を達成していないため、委譲といったオブジェクト指向で使われるテクニックを制限してしまうことにもなります。
フィールドを追加したい場合、HashMap
等によって代替します。
引き算(Subtraction)が、足し算(Addition)と単項演算子のマイナス(Minus)を用いて表現できるシンタックスシュガーであることを次のように表現できます。
Minus { "-" operand:expression } Addition { operand1:expression "+" operand2:expression} $private Subtraction -> Addition { operand1:expression "-" operand2:SubtractionRHS } $private SubtractionRHS -> Minus { operand:expression }
Subtraction
は文法上 expression
"-"
expression
ですが、Addition
をextendsしているため足し算として扱われます。その2つめのオペランド(SubtractionRHS
)は、Minus
をextendsしているので、単項マイナスとして扱われます。Subtraction
と SubtractionRHS
は $private
修飾されており、パーサの外部からアクセスできないため、Addition
や Minus
とは、わざわざ子のノードを調べない限り、区別がつきません。
生成された抽象構文木を書き換えることで、シンタックスシュガーを除去することができます。¬<><∪∪が出力したクラスを extends
し、createNode
をオーバーライドします。
createNode
は、NodeInitializationParameters
引数で与えられるノードを子に持つノードを構築するメソッドです。ユーザは super.createNode
が返すサブツリーにたいして、Default.Node.replace
等を用いて書き換えを行うことができます。根を変更したい場合、Default.Node.replace
を呼び出す代わりに、変更後のノードをこのメソッドから返します。戻り値がゴールシンボルのインスタンスになるのを待って一度に書き換えることもできますし、細かい単位で頻繁に書き換えを行うこともできます。書き換えらて、ツリーには残らないノードは $protected
修飾しておくのがよいでしょう。
書き換え後のオブジェクトの型が、書き換え前のオブジェクトの型の特定の親の型の場合、RestrictorExpression を用いることで、メソッドの戻り値の型をコントロールできます。A { label:B/C }
は、文法としては A { label:B }
と等価ですが、A のメソッド label の戻り値は、B と C の共通の型になります。
¬<><∪∪の抽象構文木は、コメントやホワイトスペースのトークンも含むため、構文木に含まれるトークン(のgetOriginalImage()
)を順に出力することで、構文解析の対象になったテキストを復元することができます。抽象構文木の一部分を書き換えることで特定のコードコンベンションに合うようにプログラムを修正したり、出力時にトークンに応じて処理を変えることで、色やリンクが付加されたHTMLに変換することができます。
--target
コマンドラインのオプション --target
により、特定のバージョンのJavaに最適なプログラムを出力することができます。次の値がサポートされています。デフォルトは、¬<><∪∪を実行しているJavaのVMのバージョンです。
引数 | 説明 |
---|---|
1.2 | 1.4 と、次の点が異なります。
|
1.3 | 1.2 と同じです。 |
1.4 | Java1.4 に最適なプログラムが出力されます。¬<><∪∪は、最初にJava1.4をターゲットとして開発されたため、最も安定しています。ドキュメント通りの出力が行われます。 |
1.5 | (テストされていません) Genericsと、オーバーライドしたメソッドでの戻り値型の変更をサポートします。 1.4 と、次の点が異なります。
|
¬<><∪∪が出力したクラスをextends
し、メソッドをオーバーライドすることで、様々なカスタマイズを加えることができます。
createLexicalAnalyzer()
をオーバーライドすることで、ユーザ独自の字句解析器を使用することができます。ユーザは、LexicalAnalyzer
インターフェイスを実装した独自のクラスを利用することもできますし、Default.LexicalAnalyzer
クラスを元にして、動作をカスタマイズすることもできます。Default.LexicalAnalyzer
では、次のメソッドをオーバーライドすることができます。
next()
は、字句解析器から次のトークンを取り出すメソッドです。このメソッドをオーバーライドし、super.next()
の結果を加工することができます。例えばJavaのassert
のような文字列をキーワードとみなすか識別子とみなすかを実行時に変更することができます。また、独自にトークンを切り出すこともでき、正規表現では表現できないようなトークンを処理することができます。独自にトークンを切り出した場合、index
などのフィールドを適切に更新しなければなりません。
nextChar()
は、入力されたテキストから次の1文字を取り出します。このメソッドをオーバーライドして、JavaのUnicodeエスケープなどの、文字列を文字にマッピングする処理を行うことができます。
Unicodeエスケープを扱いたい場合、このメソッドをオーバーライドして、nextUnicodeEscapedChar()
を呼び出すのが簡単です。
createNode()
は、NodeInitializationParameters
で与えられるノードを子に持つノードを構築します。createNode()
が構築するノードの型は、1つ目の引数で示されます。
createNode()
をオーバーライドすることで、次のことが可能になります。
createNode(int, TopLevelClass.NodeInitializationParameters, boolean)
を利用することで、ラベル付けされていないノードをツリーから除外し、メモリの消費を抑えることができますsuper.createNode()
あるいはデフォルトの実装のコンストラクタによって作られたノードを根とするサブツリーに対して、Default.Node.replace 等を用いて、根以外のノードの書き換えを行うことができます。createNode()
が返すサブツリーの根を変更することができますノードを書き換える場合、ユーザ独自のクラスのインスタンスを用いることができます。ユーザ独自のクラスは通常次のいずれかです。
NodeInitializationParameters
を取るもの
1つめのタイプは、createNode()
が返すサブツリーの根を変更する時に使われます。NodeInitializationParameters
を引数に渡して、直接ノードを構築することができます。他の2つのタイプは、super.createNode()
などでサブツリーを得た後、そのサブツリーに対して書き換えを行う場合に使用されます。
¬<><∪∪が出力するパーサに対して何らかのカスタマイズを加える場合、カスタマイズが加えられていないクラスに対するアクセスを制限したい場合があります。次の方法があります。
$package
の宣言の次に、$protected $constructor;
と書くことによって、¬<><∪∪が出力するパーサのコンストラクタを、パッケージスコープにすることができます。これによって、¬<><∪∪が出力するクラスをインスタンス化できるのは、同じパッケージに属すクラスに限られます。$protected-parsable
修飾子を用いることで、パースを行うメソッドをprotectedにすることができます。