tag:crieit.net,2005:https://crieit.net/tags/Parslet/feed 「Parslet」の記事 - Crieit Crieitでタグ「Parslet」に投稿された最近の記事 2021-11-06T07:02:42+09:00 https://crieit.net/tags/Parslet/feed tag:crieit.net,2005:PublicArticle/17743 2021-11-06T07:02:42+09:00 2021-11-06T07:02:42+09:00 https://crieit.net/posts/simple-compiler-parser-ruby-parslet Parsletでかんたんな自作言語のパーサを書いた <p><a href="https://crieit.now.sh/upload_images/8049aaf2f8f58278f22a5618f2f8293f6185a962805bf.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8049aaf2f8f58278f22a5618f2f8293f6185a962805bf.png?mw=700" alt="image" /></a></p> <p><自作言語処理系の説明用テンプレ></p> <p>自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。</p> <ul> <li>リポジトリ: <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2">github.com/sonota88/vm2gol-v2</a></li> <li>小規模: コンパイラ部分は 1,000 行程度</li> <li>pure Ruby / 標準ライブラリ以外への依存なし</li> <li>独自VM向けにコンパイルする</li> <li>ライフゲームのために必要な機能だけ <ul> <li>変数宣言、代入、反復、条件分岐、関数呼び出し</li> <li>演算子: <code>+</code>, <code>*</code>, <code>==</code>, <code>!=</code> のみ(優先順位なし)</li> <li>型なし(値は整数のみ)</li> </ul></li> <li>作ったときに書いた備忘記事 <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/f9cb3fc4a496b354b729">RubyでオレオレVMとアセンブラとコード生成器を2週間で作ってライフゲームを動かした話</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/2b95378b43a22109513c">Rubyでかんたんな自作言語のコンパイラを作った</a></li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/132314">Ruby 以外の言語への移植</a>(コンパイラ部分のみ)</li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/1e683276541cf1b87b76">セルフホスト版</a>(別リポジトリ)</li> </ul> <p><説明用テンプレおわり></p> <hr /> <p>もともとパーサ部分は手書きの再帰下降パーサでしたが、PEGベースのパーサライブラリ <a target="_blank" rel="nofollow noopener" href="https://github.com/kschiess/parslet">Parslet</a> 版を作ってみました。</p> <h1 id="できたもの"><a href="#%E3%81%A7%E3%81%8D%E3%81%9F%E3%82%82%E3%81%AE">できたもの</a></h1> <p><code>vgparser_parslet.rb</code> を追加したブランチです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/tree/alt_parser_parslet">https://github.com/sonota88/vm2gol-v2/tree/alt_parser_parslet</a></p> <p>ここでは雰囲気程度ということで Parser クラスのみ貼ります。全体は GitHub の方で見てください。transform なども合わせると全体は 340 行くらいです。</p> <p>Parslet を使うのは今回初めてで、まだ慣れてなくて、こなれていない感じがします。もっといい書き方ができそう。</p> <pre><code class="ruby">class Parser < Parslet::Parser rule(:comment) { str("//") >> (str("\n").absent? >> any).repeat >> str("\n") } rule(:spaces) { ( match('[ \n]') | comment ).repeat(1) } rule(:spaces?) { spaces.maybe } rule(:lparen ) { str("(") >> spaces? } rule(:rparen ) { str(")") >> spaces? } rule(:lbrace ) { str("{") >> spaces? } rule(:rbrace ) { str("}") >> spaces? } rule(:comma ) { str(",") >> spaces? } rule(:semicolon) { str(";") >> spaces? } rule(:equal ) { str("=") >> spaces? } rule(:ident) { ( match('[_a-z]') >> match('[_a-z0-9]').repeat ).as(:ident_) >> spaces? } rule(:int) { ( str("-").maybe >> ( (match('[1-9]') >> match('[0-9]').repeat) | str("0") ) ).as(:int_) >> spaces? } rule(:string) { str('"') >> ((str('"').absent? >> any).repeat).as(:string_) >> str('"') >> spaces? } rule(:arg) { ident | int } rule(:args) { ( ( arg.as(:arg_) >> (comma >> arg.as(:arg_)).repeat ).maybe ).as(:args_) } rule(:factor) { ( lparen >> expr.as(:factor_expr_) >> rparen ).as(:factor_) | int | ident } rule(:binop) { ( str("+") | str("*") | str("==") | str("!=") ).as(:binop_) >> spaces? } rule(:expr) { ( factor.as(:lhs_) >> (binop.as(:binop_) >> factor.as(:rhs_)).repeat(1) ).as(:expr_) | factor } rule(:stmt_return) { ( str("return") >> (spaces >> expr.as(:return_expr_)).maybe >> semicolon ).as(:stmt_return_) } rule(:stmt_var) { ( str("var") >> spaces >> ident.as(:var_name_) >> (equal >> expr.as(:expr_)).maybe >> semicolon ).as(:stmt_var_) } rule(:stmt_set) { ( str("set") >> spaces >> ident.as(:var_name_) >> equal >> expr.as(:expr_) >> semicolon ).as(:stmt_set_) } rule(:funcall) { ( ident.as(:fn_name_) >> lparen >> args.as(:args_) >> rparen ).as(:funcall_) } rule(:stmt_call) { ( str("call") >> spaces >> funcall >> semicolon ).as(:stmt_call_) } rule(:stmt_call_set) { ( str("call_set") >> spaces >> ident.as(:var_name_) >> equal >> funcall.as(:funcall_) >> semicolon ).as(:stmt_call_set_) } rule(:stmt_while) { ( str("while") >> spaces? >> lparen >> expr.as(:expr_) >> rparen >> lbrace >> stmts.as(:stmts_) >> rbrace ).as(:stmt_while_) } rule(:when_clause) { ( str("when") >> spaces? >> lparen >> expr.as(:expr_) >> rparen >> lbrace >> stmts.as(:stmts_) >> rbrace ).as(:when_clause_) } rule(:stmt_case) { ( str("case") >> spaces >> when_clause.repeat.as(:when_clauses_) ).as(:stmt_case_) } rule(:stmt_vm_comment) { ( str("_cmt") >> lparen >> string.as(:cmt_) >> rparen >> semicolon ).as(:stmt_vm_comment_) } rule(:stmt_debug) { ( str("_debug") >> lparen >> rparen >> semicolon ).as(:stmt_debug_) } rule(:stmt) { stmt_return | stmt_var | stmt_set | stmt_call | stmt_call_set | stmt_while | stmt_case | stmt_vm_comment | stmt_debug } rule(:stmts) { (stmt.repeat).as(:stmts_) } rule(:func_def) { ( str("func") >> spaces >> ident.as(:fn_name_) >> lparen >> args.as(:fn_args_) >> rparen >> lbrace >> stmts.as(:fn_stmts_) >> rbrace ).as(:func_def_) } rule(:top_stmt) { func_def.as(:top_stmt_) } rule(:program) { spaces? >> (top_stmt.repeat).as(:top_stmts_) } root(:program) end </code></pre> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <ul> <li>PEGベースのパーサを使ったことがなかったので、今回触ってみて雰囲気が知れてよかった</li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/pocari/items/048b878575bb28a40732">[ruby] Parsletで構文解析する[その1] - Qiita</a> 〜 その3 までを読むと基本的な使い方はほぼ分かる。ありがとうございます :pray:</li> <li>他には <a target="_blank" rel="nofollow noopener" href="https://github.com/kschiess/parslet">github.com/kschiess/parslet</a> の <code>example/</code> に入っているサンプルを見て参考にしたり <ul> <li><code>json.rb</code> や <code>string_parser.rb</code> など</li> </ul></li> <li>もう少し大きめのサンプルとして <a target="_blank" rel="nofollow noopener" href="https://github.com/undees/thnad">thnad</a> を参考にしたり</li> </ul> <hr /> <p>「○○以外の文字の連続」</p> <p>今回書いたものでいえば、コメント(改行以外の文字の連続)や文字列(<code>"</code> 以外の文字の連続)の部分。</p> <p><code>match</code> で正規表現が使えるので最初はこのように書いていました:</p> <pre><code class="ruby">match('[^\n]').repeat </code></pre> <p>これでも動きます。ただ、公式のサンプルを見ると <code>absent?</code> と <code>any</code> を組み合わせていたので、それに倣って次のように書きました。こういうのは知らないと何をやってるのかわかりにくいかも。</p> <pre><code class="ruby">(str("\n").absent? >> any).repeat </code></pre> <h1 id="この記事を読んだ人はこちらの記事も読んでいます(たぶん)"><a href="#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%92%E8%AA%AD%E3%82%93%E3%81%A0%E4%BA%BA%E3%81%AF%E3%81%93%E3%81%A1%E3%82%89%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%82%E8%AA%AD%E3%82%93%E3%81%A7%E3%81%84%E3%81%BE%E3%81%99%EF%BC%88%E3%81%9F%E3%81%B6%E3%82%93%EF%BC%89">この記事を読んだ人はこちらの記事も読んでいます(たぶん)</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/97ba1f0c377fbe86d5b1">Raccでかんたんな自作言語のパーサを書いた</a></li> </ul> sonota486