トークンのリストからASTを作成します。スクリプト言語を作成しており、字句解析の部分はすでに完了していますが、ASTの作成方法がわかりません。質問は次のとおりです。
Word, int
Word, x
SYMBOL, =
NUMBER, 5
SYMBOL, ;
それを抽象構文ツリーに変換しますか? without ANTLRのようなライブラリなど、できれば自分でやりたいと思います。ただし、それが本当に複雑なタスクであれば、ライブラリを使用しても構いません:)ありがとう
基本的なトリックは、トークンが1つずつ読み取られるなど、構文解析はどのように行われても増分ステップで発生することを認識することです。
各増分ステップで、AST AST他の増分ステップによって作成されたフラグメント。これは再帰的なアイデアです。 ASTトークンがスキャンされるときのトークンのリーフノード。この基本的な考え方は、ほとんどすべてのAST構築パーサーで発生します。
再帰降下パーサーを構築する場合、実際には、再帰プロシージャの協調システムを構築します。各再帰プロシージャは、実装されている文法にかかわらず、非終端記号を認識します。純粋な構文解析の場合、各プロシージャは「非終端(認識されない)認識」のブール値を返すだけです。
再帰降下パーサーでASTを構築するには、これらのプロシージャを設計して2つの値を返します。ブール値「認識済み」、および認識された場合はAST (何らかの方法で)非端末用に構築されます(一般的なハックは、「認識されない」ために無効なポインターを返すか、構築されたAST if "認識された"を指します)。結果のASTは、単一のプロシージャに対して構築され、それが呼び出すサブプロシージャからのASTを結合することによって行われます。これは、入力トークンを読み取り、すぐに実行できるリーフプロシージャに対しては非常に簡単です。ツリーを構築します。
これのすべての欠点は、再帰下降を手動でコーディングし、ツリー構築ステップでそれを補強する必要があることです。物事の壮大なスキームでは、これは実際には小さな文法のためにコーディングするのは非常に簡単です。
OPの例では、次の文法があると仮定します。
GOAL = ASSIGNMENT
ASSIGNMENT = LHS '=' RHS ';'
LHS = IDENTIFIER
RHS = IDENTIFIER | NUMBER
OK、再帰降下パーサー:
boolean parse_Goal()
{ if parse_Assignement()
then return true
else return false
}
boolean parse_Assignment()
{ if not Parse_LHS()
then return false
if not Parse_equalsign()
then throw SyntaxError // because there are no viable alternatives from here
if not Parse_RHS()
then throw SyntaxError
if not Parse_semicolon()
the throw SyntaxError
return true
}
boolean parse_LHS()
{ if parse_IDENTIFIER()
then return true
else return false
}
boolean parse_RHS()
{ if parse_IDENTIFIER()
then return true
if parse_NUMBER()
then return true
else return false
}
boolean parse_equalsign()
{ if TestInputAndAdvance("=") // this can check for token instead
then return true
else return false
}
boolean parse_semicolon()
{ if TestInputAndAdvance(";")
then return true
else return false
}
boolean parse_IDENTIFIER()
{ if TestInputForIdentifier()
then return true
else return false
}
boolean parse_NUMBER()
{ if TestInputForNumber()
then return true
else return false
}
それでは、抽象構文ツリーを作成するように修正しましょう。
AST* parse_Goal() // note: we choose to return a null pointer for "false"
{ node = parse_Assignment()
if node != NULL
then return node
else return NULL
}
AST* parse_Assignment()
{ LHSnode = Parse_LHS()
if LHSnode == NULL
then return NULL
EqualNode = Parse_equalsign()
if EqualNode == NULL
then throw SyntaxError // because there are no viable alternatives from here
RHSnode = Parse_RHS()
if RHSnode == NULL
then throw SyntaxError
SemicolonNode = Parse_semicolon()
if SemicolonNode == NULL
the throw SyntaxError
return makeASTNode(ASSIGNMENT,LHSNode,RHSNode)
}
AST* parse_LHS()
{ IdentifierNode = parse_IDENTIFIER()
if node != NULL
then return IdentifierNode
else return NULL
}
AST* parse_RHS()
{ RHSnode = parse_IDENTIFIER()
if RHSnode != null
then return RHSnode
RHSnode = parse_NUMBER()
if RHSnode != null
then return RHSnode
else return NULL
}
AST* parse_equalsign()
{ if TestInputAndAdvance("=") // this can check for token instead
then return makeASTNode("=")
else return NULL
}
AST* parse_semicolon()
{ if TestInputAndAdvance(";")
then return makeASTNode(";")
else return NULL
}
AST* parse_IDENTIFIER()
{ text = TestInputForIdentifier()
if text != NULL
then return makeASTNode("IDENTIFIER",text)
else return NULL
}
AST* parse_NUMBER()
{ text = TestInputForNumber()
if text != NULL
then return makeASTNode("NUMBER",text)
else return NULL
}
私は明らかにいくつかの詳細を明らかにしましたが、読者がそれらを埋めるのに問題はないと思います。
JavaCCやANTLRなどのパーサージェネレーターツールは、基本的に再帰降下型パーサーを生成し、このように機能するツリーを構築する機能を備えています。
ボトムアップパーサー(YACC、Bison、GLRなど)をビルドするパーサージェネレーターツールも、同じスタイルでASTノードをビルドします。ただし、再帰関数のセットはありません。代わりに、 ASTノードは並列スタック上に構築されます。削減が発生すると、AST =縮約の対象となるスタックの一部のノードは、結合されて非終端を生成しますASTそれらを置き換えるノード。これは、空の文法ルールの「ゼロサイズ」スタックセグメントでも発生しますASTノード(通常は「空のリスト」または「欠落しているオプション」の場合)がどこからともなく見えるようになります。
ちょっとした言語では、ツリーを構築する再帰下降パーサーの作成は非常に実用的です。
実際の言語の問題(COBOLのような古くて不名誉なものでも、Scalaのような熱くて光沢のあるものでも)は、文法規則の数が非常に多く、言語の洗練度と言語委員会が担当する言語委員会の主張によって複雑になることです他の言語が提供する新しいものを絶えず追加します(「言語のvy望」、Java、C#、C++の進化的な競争を参照)。現在、再帰降下パーサーの作成は手に負えなくなり、パーサージェネレーターを使用する傾向があります。ただし、パーサージェネレーターを使用しても、すべてのカスタムコードを記述してASTノードを作成することも大きな戦いです(優れた「抽象」構文を設計するために必要なことについては説明していません。最初に頭に浮かぶこと)文法規則とAST gooの構築は規模と進行中の進化によりますます難しくなります。そのため、AST構築ルールを書くことさえ厄介になります。
理想的には、文法を書き、パーサーとツリーを取得したいだけです。 最近のパーサージェネレーターでこれを行うことができます:DMS Software Reengineering Toolkitは、完全なコンテキストのない文法を受け入れ、ASTを自動的に構築します 、文法エンジニアの仕事はありません。 ANTLRの担当者は2014年にようやくこれを理解し、ANTLR4はこのようなオプションを提供しています。
最後の点:パーサーを(ASTを使用して)持つことは、解決しようとした実際の問題の解決策ではありません。それは単なる基礎部分であり、ほとんどのパーサー初心者にとっては衝撃的なものであり、コードを操作するツールにとってはsmallest部分です。詳細については、Life After Parsingに関する私のエッセイ(または私の略歴を確認してください)をGoogleで検索してください。
難しくありません。実際、これは私がやった最も簡単なことの1つです。一般的な考え方は、各構造(別名パーサールール)は単なる他の構造のリストであり、parse()関数が呼び出されると、それらの子をループし、解析するよう指示するだけです。これは無限ループではありません。トークンは構造体であり、それらのparse()が呼び出されると、レクサーの出力がスキャンされます。識別のための名前も必要ですが、これは必須ではありません。 parse()は通常、解析ツリーを返します。構文解析ツリーは、構造のように子のリストです。また、識別のために「テキスト」フィールドとその親構造を用意しておくと便利です。以下に例を示します(実際のプロジェクトでは、より適切に整理し、nullを処理する必要があります)。
public void Push(ParseTree tree) { // ParseTree
children.add(tree);
text += tree.text;
}
public ParseTree parse() { // Structure
ParseTree tree = new ParseTree(this);
for(Structure st: children) {
tree.Push(st.parse());
}
return tree;
}
public ParseTree parse() { // Token
if(!lexer.nextToken() || !matches(lexer.token))
return null;
ParseTree tree = new ParseTree(this);
tree.text = lexer.token;
return tree;
}
そこ。メイン構造体のparse()を呼び出すと、ASTが得られます。もちろん、これは非常に単純な例であり、そのままでは機能しません。 「修飾子」を持つことも便利です。例えば子3に1回以上一致します。子2はオプションです。それも簡単です。あなたの子供の数と同じサイズの配列にそれらを保存し、解析するときにそれをチェックしてください:
public void setModifier(int id, int mod) {
mods[id] = mod;
}
public ParseTree parse() {
...
ParseTree t;
switch(mods[i]) {
case 1: // Optional
if((t = st.parse()) != null) tree.Push(t);
case 2: // Zero or more times
while((t = st.parse()) != null) tree.Push(t);
...
default:
tree.Push(st.parse());
}
...
}