インタラクティブモードを備えた軽量パーサーデザイナー

簡単なテキストアナラ​​イザーを開発するためにときどき小さなタスクに直面したため、私はこのタスクを自動化することにしましたが、学術的な関心が休むことはありませんでした。 最初はRaccYaccの解釈の1つ)に目を向けていましたが、小さなタスクにはかなり難しいソリューションのように思えたため、単純なアナライザーを開発することにしました。 もちろん、コンパイラーなどを開発していて、工業規模でも開発している場合は、間違いなくRaccに目を向ける必要があります。



ただし、ドラゴンブックなどの字句解析器に関する記事を読むことなく、パーサーとは何か、自分ですばやく自分で記述する方法を自分で理解したい場合は、(本は非常に優れていますが)理解を深めてください。



入力ストリーム分析



入力ストリームを非常に抽象的な方法で分析するタスクを検討し、これから開始する必要があると思う場合、すべては有限状態マシンの実装に帰着します。 ステートマシン。特定の状態への遷移の条件は、入力ストリームに必要なデータ(以下、テンプレートと呼ぶ)が存在することです。 次に、入力ストリームの文字が明確で特定のテンプレートと完全に一致する場合にのみ、状態への遷移を実行できます。 1つの状態から一度に複数の状態に切り替えることができ、ストリームの入力シンボルが同時に複数の状態パターンと一致する間、アナライザーは不確実な状態になります。 上記に基づいて、アナライザー自体は次の状態になる場合があります。





もちろん、誤った条件も可能ですが、 一般化されたアナライザーモデルを検討し、どの文字セットでエラーを生成すべきかわからない場合、このタスクは具体的な実装にかかっています。



確実な状態では、どの状態ハンドラーを呼び出す必要があるかが正確にわかりますが、不確実な状態では、次の状態がどうなるかわかりません。 どのテンプレートが一致するか、最終的に一意の一致は決してありません。この場合、入力ストリームをバッファリングしてから、蓄積されたバッファからすでに定義された状態に文字を転送する必要があります。 不確実な場合、入力ストリームをバッファリングし、すでに状態遷移モードになっています。バッファデータを新しい状態に転送するか、遷移が発生しなかった場合は現在の状態に戻す必要があります。



これで、入力ストリームを分析するプロセスをより正確に詳細に分解して、次のことができます。

入力ストリームの分類(1つと一致し、多くのテンプレートと一致し、テンプレートと一致しません)および分類結果の処理(遷移、バッファリングなど)。



実装



私は主な職業で組み込みシステムの開発者であり、主なプログラミング言語はANSI Cですが、このモデルをRubyで実装することにしました。 その実装の技術的な詳細についてはあまり考えず、このモデルをRuby Gemとして実装する予定です。 普遍的(特定のフレームワーク内)、短く簡潔なものすべてが大好きです。 将来的には、このモデルを入力トラフィックアナライザー専用に実装しますが、より単純な形式で、おそらくHDLで実装する予定です。



だから、始めましょう。もう一度繰り返します。Rubyについてはあまり詳しくないので、蓄積された経験に賭けて、頭の中のプロセスの明確なアルゴリズム化をしました。そのため、私の音楽教師はかつて言ったように、 「3〜5音で即興で音楽を作ることを学ぶと、クールなパッセージを演奏します。そうしないと、連続したアルペジオを聴くことはできません。」 時々彼を思い出して、今は彼が正しかったと思いますが、そのとき私はクールなソロを望んでいました。



開発プロセスは段階的に進むべきだと思います。その段階は「具体的」です。 このステージを実装するために何をする必要があり、どのボリューム(労力)であるかを理解する必要があります。これについては、このトピックではなく、ここでは詳しく説明しません。 そして、ここではインタラクティブモードなしではできません。誰かが私のgemを使用する場合、最初にインタラクティブモードでモデルの動作をデバッグしてから、パーサーステートハンドラーを作成する必要があると思います。





わかりやすくするために、リモートでdoxygenに似たドキュメントを自動生成するための簡単なソースコードファイルパーサーの実装例を示します。 最初に、私のgemをインストールする必要があります。



$ gem install iparser
      
      





すでに理解しているように、gemはiparserと呼ばれ、インストール後に 'parser_example.rb'というファイルを作成します。このファイルには、 iparserに基づいてパーサーのコードを記述します。 まず、ライブラリを接続し、アナライザーのメインロジックを実装するパーサーマシンのオブジェクトを作成します。 パーサー自体のインスタンス:



 require 'iparser' parser = Iparser::Machine.new
      
      





次のステップは、パーサーの状態を記述することです。 非常に単純なパーサーがあり、3つの状態しかありません。





これで、各状態を個別に説明できます(以下では、スクリプトコード全体を説明します)。 「アイドル」の状態については詳しく説明しません。 私たちの場合、それは空であり、「コメント行」の状態をより近く検討します。 状態インスタンスを作成するには、次のコードを使用します。



 ps_idle = Iparser::State.new('idle') ps_cline = Iparser::State.new('comment-line')
      
      





状態コンストラクターのパラメーターでは、状態の名前(識別子)で行が示されます。 モデルのさらなるデバッグに使用されます。 各パーサー状態オブジェクトには入力フィールドがあります。これは、この状態への遷移が行われる偶然のパターンです。 条件入力条件。 テンプレートとの比較は、入力ストリーム処理と同様に文字ごとに行われます。 入力ストリームに連続した文字「\\\」がある場合、状態の入力が実行されます。 状態を終了する(前のテンプレートに戻る)テンプレートを指定するためのleaveフィールドもあります。この場合、これらは文字列 '\ n \ r'の末尾の文字ですが、最初の文字を指定するだけで十分ですが、明確にするために両方を示します。 次に、「コメント行」について説明します。



 ps_cline.entry << /\// ps_cline.entry << /\// ps_cline.entry << /\// ps_cline.leave << /[\n\r]/
      
      





パターンは正規表現を使用して設定されることに注意してください。パターンなしでも可能ですが、これは後で行われます。



次に、複数行コメントを処理するための状態を作成し、文字「/ **」が出会うとその状態に入り、「* /」がある場合はそのままにします。 また書きます:



 ps_cblock = Iparser::State.new('comment-block') ps_cblock.entry << /\// ps_cblock.entry << /\*/ ps_cblock.entry << /\*/ ps_cblock.leave << /\*/ ps_cblock.leave << /\// ps_cblock.ignore << '*'
      
      





また、この状態では無視する必要がある文字も示しました。 このアスタリスク記号があるのは、 フォームの複数行コメントを書くのが好きです:



 /** * ... * ... */
      
      





ここで、3つの状態を1つのチェーンに接続する必要があります。「アイドル」から「コメント行」と「コメントブロック」に入り、それらから「アイドル」に戻るだけです。 リンクは、各状態のブランチフィールドに状態インデックスを指定することによって行われます。 インデックスは、状態オブジェクトがパーサーインスタンスに追加される順序によって決定されます;オブジェクトをパーサーに追加するには、addstateパーサーメソッドが使用されます。



 ps_idle.branches << 1 ps_idle.branches << 2 parser.addstate ps_idle parser.addstate ps_cline parser.addstate ps_cblock
      
      





最後に、ステートチェーンを正しく作成したかどうかを確認する必要があります。このため、インタラクティブモードを開始します( prestartメソッドすべての初期パラメーター設定します)。



 parser.prestart parser.interactive_parser
      
      





明確にするために、スクリプトコード全体を提供します。もちろん、少し再構築しましたが、上に書いたコードのみが含まれています。



 require 'iparser' # # Create parser-machine object. # parser = Iparser::Machine.new # # Create startup state for this parser-machine. # ps_idle = Iparser::State.new('idle') # # Add branch indexes to 'comment-line' and 'comment-block' state. # ps_idle.branches << 1 ps_idle.branches << 2 # # Create single line comment state for this parser-machine. # ps_cline = Iparser::State.new('comment-line') ps_cline.entry << /\// ps_cline.entry << /\// ps_cline.entry << /\// ps_cline.leave << /[\n\r]/ # # Create multiline comment state for this parser-machine. # ps_cblock = Iparser::State.new('comment-block') ps_cblock.entry << /\// ps_cblock.entry << /\*/ ps_cblock.entry << /\*/ ps_cblock.leave << /\*/ ps_cblock.leave << /\// ps_cblock.ignore << '*' # # Add all states to parser-machine. # parser.addstate ps_idle parser.addstate ps_cline parser.addstate ps_cblock # # Call parser startup method. # parser.prestart # # Call interactive mode for check state-machine. # parser.interactive_parser
      
      





それほど大きなコードではないので、スクリプトを実行します。



 $ ruby parser_example.rb
      
      





インタラクティブモードを終了するには、空の行を入力する必要があります(すぐにEnterキーを押します )。



「\\\」と入力し、最後の文字でパーサーが「comment-line」状態( comment-lineに分岐 )になることを確認し、「\ n」または「\ r」と入力すると、パーサーが状態「アイドル '( 戻る )。 文字「\」はesc-charactersを入力するために使用されるため、文字「\」を入力するには、「\\」を2回入力する必要があります。 インタラクティブモードでは、すべての状態のチェーンが正しく接続されていることを確認するまで、好きなだけ楽しむことができます。



オートマトンの遷移ロジックをチェックしてデバッグした最後のステップでは、状態のハンドラーを追加できます(開発プロセスについて上で書いたように、今だけ)、それらは非常に簡単です。 各状態には3つのハンドラーがあります。





状態コンストラクターは、状態に入るときに一度だけ呼び出され、不確定モードのときにパーサーが蓄積したバッファーされた文字の配列を引数として受け取ります。 ハンドラーは常に呼び出され、状態を出るまで入力ストリームシンボルを引数として受け取り、デストラクタはこの状態を出るときに一度だけ呼び出されます。



ハンドラーメソッドもパーサーの動作を制御できますが、それがあれば、それを実装したのは無駄である可能性があるため、説明する必要があります。 ハンドラーが値が範囲(> = 0)のFixnumデータ型返す場合、これはある状態への遷移のインデックスと見なされます。インデックスが状態配列の境界を超えると、パーサーは例外をスローします。 ハンドラーがnilを返す場合、パーサーはこの状態を保持します。 反応がなく、他の値がある場合、これはエラーとみなされ、パーサーは解析エラーを通知するfalseを返します 。 その他の場合はすべて、 trueを返します 。 以下に、変更されたコードを完全に示します。 ので、私はすべてを詳細に説明しようとしました。



 require 'iparser' # # Simple check startup arguments. # if( ARGV.size != 1 || !File.exist?(ARGV[0]) ) puts puts "ERROR: unable to open file #{ARGV[0]}" puts exit end # # Create output file. # $fout = File.new( 'index.html', 'w' ) # # Create initializer method for parser-states. # def doc_init ( str ) $fout.print "<p>" end # # Create handler method for parser-states. # def doc_handler ( c ) $fout.print c end # # Create finalizer method for parser-states. # def doc_fini ( str ) $fout.puts "</p>" end # # Create parser-machine object. # parser = Iparser::Machine.new # # Create startup state for this parser-machine. # ps_idle = Iparser::State.new('idle') # # Add branch indexes to 'comment-line' and 'comment-block' state. # ps_idle.branches << 1 ps_idle.branches << 2 # # Create single line comment state for this parser-machine. # ps_cline = Iparser::State.new('comment-line') ps_cline.entry << /\// ps_cline.entry << /\// ps_cline.entry << /\// ps_cline.leave << /[\n\r]/ # # Create multiline comment state for this parser-machine. # ps_cblock = Iparser::State.new('comment-block') ps_cblock.entry << /\// ps_cblock.entry << /\*/ ps_cblock.entry << /\*/ ps_cblock.leave << /\*/ ps_cblock.leave << /\// ps_cblock.ignore << '*' # # Add handlers for states. # ps_cline.init( method(:doc_init) ) ps_cline.handler( method(:doc_handler) ) ps_cline.fini( method(:doc_fini) ) ps_cblock.init( method(:doc_init) ) ps_cblock.handler( method(:doc_handler) ) ps_cblock.fini( method(:doc_fini) ) # # Add all states to parser-machine. # parser.addstate ps_idle parser.addstate ps_cline parser.addstate ps_cblock # # Call parser startup method. # parser.prestart # # Call interactive mode for check state-machine. # $fout.puts "<html>" $fout.puts "<body>" File.open( ARGV[0], 'r' ).each do |line| line.each_char do |c| parser.parse(c) end end $fout.puts "</body>" $fout.puts "</html>" $fout.close
      
      





はい、ハンドラー(doc_handler)はnilを返さないことに注意してください。 パーサーの結果は分析しません( parser.parseメソッド)。 最終チェックでは、「test.c」という名前のテストファイルを作成し、次の内容を入力します。



 #include <stdlib.h> ///Test function - 1. void test1 ( void ) { } /** * Test function - 2. */ void test2 ( void ) { }
      
      





スクリプトを最後に実行して、パーサーの結果を確認します。



 $ ruby parser_example.rb test.c
      
      





これは「index.html」というファイルです。これですべてです。ご清聴ありがとうございました!



誰かの助けになり、アナライザーの基本原理を勉強する時間を短縮するか、誰かがもっとシンプルなオプションを見つけずに個人的な目的で私の宝石を使いたいと思うことを願っています。あなたは良い批判なしに良いクリエーターにならないからです。 興味のある方は、gem iparserへのリンクをご覧ください



All Articles