tag:crieit.net,2005:https://crieit.net/tags/parser/feed
「parser」の記事 - Crieit
Crieitでタグ「parser」に投稿された最近の記事
2023-06-25T08:41:34+09:00
https://crieit.net/tags/parser/feed
tag:crieit.net,2005:PublicArticle/18485
2023-06-25T08:41:34+09:00
2023-06-25T08:41:34+09:00
https://crieit.net/posts/ruby-lrama-parser
Lramaで簡単な自作言語のパーサを書いた
<p><a href="https://crieit.now.sh/upload_images/36b6f940e2f434c979b5ada813c8206f64977e25696d7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/36b6f940e2f434c979b5ada813c8206f64977e25696d7.png?mw=700" alt="image" /></a></p>
<p>先日 Ruby にマージされた LALR(1)パーサジェネレータ Lrama を使ってみました。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/ruby/lrama">https://github.com/ruby/lrama</a></p>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://techracho.bpsinc.jp/hachi8833/2023_05_15/130116">RubyにlramaがマージされてBison依存がなくなった(RubyKaigi 2023)|TechRacho by BPS株式会社</a></p>
<h1 id="できたもの"><a href="#%E3%81%A7%E3%81%8D%E3%81%9F%E3%82%82%E3%81%AE">できたもの</a></h1>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-c/tree/alt-parser-lrama">https://github.com/sonota88/vm2gol-v2-c/tree/alt-parser-lrama</a></p>
<p>alt-parser-lrama ブランチに <code>mrcl_parser_lrama.y</code> が入っています。</p>
<h1 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h1>
<p>Mini Ruccola は私がコンパイラ実装に入門するために作った自作言語とその処理系です。原始的だけどその分入門者(=私)視点では分かりやすい、という方向性のものです。私でも作れるコンパイラ。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2">https://github.com/sonota88/vm2gol-v2</a></p>
<p>作ったときに書いた備忘記事:</p>
<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">(2週間ちょっとで)Rubyでかんたんな自作言語のコンパイラを作った</a></li>
</ul>
<hr />
<p>コンパイラのパーサ部分は元々手書きの再帰下降パーサだったのですが、他のパーサライブラリも試したくて Racc 版と Parslet 版のパーサを以前書きました。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/97ba1f0c377fbe86d5b1">Raccでかんたんな自作言語のパーサを書いた</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/edce05e86d3248f49401">Parsletでかんたんな自作言語のパーサを書いた</a></li>
</ul>
<p>今回の Lrama 版はこのシリーズの延長です。</p>
<hr />
<p>Mini Ruccola コンパイラはレキサ・パーサ・コード生成器が独立しています。そのため、レキサとコード生成器は Ruby 製のものをそのまま使い、パーサだけ別の言語で書いて一緒に動かすなんてことも可能です。</p>
<p>可能なのですが、C言語への移植版<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-c">github.com/sonota88/vm2gol-v2-c</a><br />
を使った方が楽なので今回はこっちを使いました。</p>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/06/043607">素朴な自作言語のコンパイラをCに移植した</a></p>
<hr />
<p>Racc 版パーサ実装と C移植版がすでにありますから、あとは Racc 版パーサを Lrama + C 向けに書き直せば一丁あがり、という寸法です。</p>
<p>たとえば以下は関数定義の規則とアクションの記述の比較です。</p>
<pre><code class="ruby"># Racc
func_def : "func" IDENT "(" args ")" "{" stmts "}"
{
_, fn_name, _, args, _, _, stmts, _, = val
result = ["func", fn_name, args, stmts]
}
</code></pre>
<pre><code class="c">// Lrama
func_def : TS_KW_FUNC TS_IDENT TS_PAREN_L args TS_PAREN_R TS_SYM stmts TS_SYM
// "func" fn_name "(" args ")" "{" stmts "}"
{
NodeList* func_def = NodeList_new();
NodeList_add_str(func_def, "func");
NodeList_add_str(func_def, $2);
NodeList_add_list(func_def, $4);
NodeList_add_list(func_def, $7);
$$ = func_def;
}
</code></pre>
<p>こんな感じで読み換えていきました。どちらも Yacc から派生した LALRパーサジェネレータなのでよく似ていますね。</p>
<h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1>
<ul>
<li>1日くらいでババッと書いたものなので雑だったり手抜きで済ませている部分があります。一応動いてる、という程度の出来です。
<ul>
<li>警告もひとまず放置</li>
</ul></li>
<li>Yacc, Bison もそのうち触ってみようと思いつつ結局今まで触らずじまいだった
<ul>
<li>今回ちょっとやってみて雰囲気が知れてよかった</li>
<li>Rubyソースコード完全解説の <a target="_blank" rel="nofollow noopener" href="https://i.loveruby.net/ja/rhg/book/yacc.html">第9章 速習yacc</a> を読んだらとりあえずなんとかなった</li>
</ul></li>
<li>Rubyには慣れているけどCは分からないという状態で LALR パーサジェネレータに入門したい人は、まず Racc で入門するのが良いのではないかと思います。Ruby の知識だけで使えるので、いきなり Yacc や Bison や Lrama に挑戦するよりはお手軽かと。
<ul>
<li>Racc の作者(青木峰郎さん)による解説本『Rubyを256倍使うための本 無道編』もまだ中古で入手可能(<a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/dp/4756137091">Amazon</a>)</li>
</ul></li>
</ul>
<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%EF%BC%88%E3%81%B2%E3%82%87%E3%81%A3%E3%81%A8%E3%81%97%E3%81%9F%E3%82%89%EF%BC%89%E3%81%93%E3%81%A1%E3%82%89%E3%82%82%E8%AA%AD%E3%82%93%E3%81%A7%E3%81%84%E3%81%BE%E3%81%99">この記事を読んだ人は(ひょっとしたら)こちらも読んでいます</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/6a96d2bcea9d134e38b7">Ruby/Racc: パース時のスタックの動きをFlameGraphっぽくビジュアライズする</a></li>
</ul>
<p>Racc の気持ちが分かるように↓こういう図を描かせてみたもの。</p>
<p><a href="https://crieit.now.sh/upload_images/7a727bbde73b2e30d4a57505e10347d264977e43df695.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7a727bbde73b2e30d4a57505e10347d264977e43df695.png?mw=700" alt="image" /></a></p>
<hr />
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/816b6b8d7a362ef4e332">Ruby: 入れ子の配列だけをパースできるパーサを作る(手書きの再帰下降パーサ)</a></li>
</ul>
<p>メソッドの再帰呼び出しについてすでに知っていれば1時間もかからず書ける内容。比較的簡単で時間がかからないので、LALR パーサにこだわる事情がなければ先に再帰下降パーサで入門するのも良いと思います。</p>
<hr />
<ul>
<li><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/84e4c16e5602b9771e92">『RubyでつくるRuby』のMinRubyのパーサを書いた(手書きの再帰下降パーサ)</a></p></li>
<li><p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/sonota88/articles/06c540eda79b98">四則演算と剰余のみのexprコマンドをRubyで作ってみた</a></p></li>
<li><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/5902f255e196b5b8b048">正規表現エンジン(ロブ・パイクのバックトラック実装)をRubyで写経した</a></p></li>
</ul>
sonota486
tag:crieit.net,2005:PublicArticle/18373
2023-01-22T17:38:20+09:00
2023-01-22T17:44:40+09:00
https://crieit.net/posts/ruby-minruby-recursive-descent-parser
『RubyでつくるRuby』のMinRubyのパーサを書いた(手書きの再帰下降パーサ)
<p>これは <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2022/ruby">Ruby Advent Calendar 2022</a> の25日目の記事です。</p>
<p>書籍 <strong>『RubyでつくるRuby ゼロから学びなおすプログラミング言語入門』</strong>(以下、 <strong>『RubyでつくるRuby』</strong> )で扱われているミニ言語 MinRuby のパーサを書いてみました。</p>
<hr />
<p>『RubyでつくるRuby』は、Ruby を使った基本的なプログラミングの入門から始まり、後半ではミニ言語 MinRuby(Ruby のサブセット)のインタプリタを作って最終的に MinRuby言語で MinRuby インタプリタを書くところまでやってしまう、という内容の入門書です。</p>
<hr />
<p>……と自分なりに内容紹介してみましたが、以下もあわせて参考にしていただければと思います。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://mametter.hatenablog.com/entry/20170315/p1">書籍『Ruby でつくる Ruby』が発売されます - まめめも</a>
<ul>
<li>著者の遠藤さん</li>
</ul></li>
<li><a target="_blank" rel="nofollow noopener" href="https://golden-lucky.hatenablog.com/entry/2023/01/10/131903">『RubyでつくるRuby』の読み方(私論) - golden-luckyの日記</a>
<ul>
<li>ラムダノートの鹿野さん</li>
</ul></li>
</ul>
<hr />
<p><a target="_blank" rel="nofollow noopener" href="https://www.lambdanote.com/products/ruby-ruby">RubyでつくるRuby ゼロから学びなおすプログラミング言語入門 – 技術書出版と販売のラムダノート</a></p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://www.lambdanote.com/products/ruby-ruby-ebook">PDF版のみの販売ページ</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/dp/4908686017">Amazon</a></li>
</ul>
<blockquote>
<p>プログラミングを始めるなら、プログラミング言語を自分でつくってみるのがいちばん! 最低限の機能なら、こんなに簡単にインタプリタを作れます。よくわからなかったプログラミングも、裏側の仕組みから分かってしまえば怖くない! </p>
<p>2016年9月から2017年1月にかけて<a target="_blank" rel="nofollow noopener" href="https://ascii.jp/serialarticles/1230449/">アスキーjpの「プログラミング+」コーナーで連載された大好評のWebコンテンツ『Rubyで学ぶRuby』</a>を、さらにわかりやすく紙版の書籍として編纂しなおして発売するものです。豊富なイラストもカラーで完全採録。</p>
</blockquote>
<ul>
<li>プログラミング初心者を想定して丁寧に解説してある</li>
<li>分量が多すぎず(144ページ)読み通しやすい</li>
<li>評価器にフォーカスしており、パーサはスコープ外
<ul>
<li>パースは <a target="_blank" rel="nofollow noopener" href="https://rubygems.org/gems/minruby">minruby gem</a> にまかせる</li>
</ul></li>
</ul>
<p>たしか発売された頃に読んで「せっかくだからパーサも自作したい」と思った記憶があります。当時はパーサの作り方についての知識が足りずどうすればよいか分からなかったのですが、その後あれこれあって簡単なものなら作れるようになりました。</p>
<h1 id="できたもの"><a href="#%E3%81%A7%E3%81%8D%E3%81%9F%E3%82%82%E3%81%AE">できたもの</a></h1>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/cookpad-hackarade-minruby/tree/recursive-descent-parser">https://github.com/sonota88/cookpad-hackarade-minruby/tree/recursive-descent-parser</a></p>
<pre><code class="terminal"> $ wc -l rcl_*.rb my_minruby_parser.rb
33 rcl_common.rb
65 rcl_lexer.rb
496 rcl_parser.rb
17 my_minruby_parser.rb
611 合計
</code></pre>
<p>一応動いてはいます。advent calendar には間に合った……リファクタリングなどは冬休み以降の宿題にします。</p>
<p>(2023-01-22 追記) interp.rb がパースできるようになりました。</p>
<h1 id="動作例"><a href="#%E5%8B%95%E4%BD%9C%E4%BE%8B">動作例</a></h1>
<pre><code class="terminal"> $ cat test3-4.rb
n = 1
while n < 100
if n % 3 == 0
if n % 5 == 0
p("FizzBuzz")
else
p("Fizz")
end
else
if n % 5 == 0
p("Buzz")
else
p(n)
end
end
n = n + 1
end
$ ruby my_minruby_parser.rb test3-4.rb
[:stmts, [:var_assign, "n", [:lit, 1]],
[:while,
[:<, [:var_ref, "n"], [:lit, 100]],
[:stmts,
[:if,
[:==, [:%, [:var_ref, "n"], [:lit, 3]], [:lit, 0]],
[:if,
[:==, [:%, [:var_ref, "n"], [:lit, 5]], [:lit, 0]],
[:func_call, "p", [:lit, "FizzBuzz"]],
[:func_call, "p", [:lit, "Fizz"]]],
[:if,
[:==, [:%, [:var_ref, "n"], [:lit, 5]], [:lit, 0]],
[:func_call, "p", [:lit, "Buzz"]],
[:func_call, "p", [:var_ref, "n"]]]],
[:var_assign, "n", [:+, [:var_ref, "n"], [:lit, 1]]]]]]
</code></pre>
<h1 id="cookpad-hackarade-minruby"><a href="#cookpad-hackarade-minruby">cookpad-hackarade-minruby</a></h1>
<p>まずは先行事例を軽く調べました。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://techlife.cookpad.com/entry/2018/10/16/131000">Hackarade #04: Create Your Own Interpreter - クックパッド開発者ブログ</a></p>
<blockquote>
<p>完全セルフホスト:MinRubyでパーサを書き、minruby gemに依存せずにinterp.rb単体でセルフホストするようにした</p>
</blockquote>
<p>さすがのクックパッドさん。今回私はここまではやっていません。パーサを MinRuby で書くのもおもしろそう……。</p>
<p>同記事により、 mame/cookpad-hackarade-minruby というリポジトリでテストケースが用意されていることを知りました。ありがたく有効活用させていただきましょう。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/mame/cookpad-hackarade-minruby">mame/cookpad-hackarade-minruby: material for Cookpad's Hackarade #4</a></p>
<p>今回はこのテストケースを使い、自作パーサと minruby gem に同じ入力を与えて同じ結果になればヨシ! ということにしました。</p>
<h1 id="方針"><a href="#%E6%96%B9%E9%87%9D">方針</a></h1>
<p>パーサ入門でよくある再帰下降パーサにしました。</p>
<p>専用のパーサライブラリを使うのに比べるとデメリットもあり本格的な言語実装ではあまり採用されない手法だと思いますが、入門者目線だと「全部自分で書いたぞ感」「全部把握できてる感覚」が得られるという、とても良い良さがあります。</p>
<p>(ちなみに、Rust製 Ruby 実装 <a target="_blank" rel="nofollow noopener" href="https://github.com/sisshiki1969/ruruby">ruruby</a> のパーサは手書きだそうです。すごい……。)</p>
<p>ここで選択肢が2つあり、</p>
<ul>
<li>(1) 何もないところから全部書く</li>
<li>(2) Ruccola のパーサを改造する</li>
</ul>
<p>ちょっとだけ (1) を試した後、時間(というか計画性)がないこともあって (2) に切り替えました。<br />
さて、Ruccola というのがスッと出てきましたね。これはなんでしょうか。</p>
<h1 id="Ruccola とは"><a href="#Ruccola+%E3%81%A8%E3%81%AF">Ruccola とは</a></h1>
<p>Ruccola は私が自分の勉強のために作っている素朴な自作プログラミング言語です。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/ruccola">https://github.com/sonota88/ruccola</a></p>
<p>最新の知見を盛り込んだキラリと光る言語……とかでは全然なく、現代の水準からするとかなり原始的なものです。自分の勉強用・入門用なので素朴でよいと割り切っています。</p>
<p>2021年にセルフホストできた(詳しくは↓の記事を参照)後も趣味の盆栽プログラミング的にちびちびと機能追加やリファクタリングを続けています。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/1e683276541cf1b87b76">素朴な自作言語Ruccolaのコンパイラをセルフホストした - Qiita</a></p>
<ul>
<li>コンパイラを Ruby で書いている</li>
<li>Ruccola言語のコードの見た目は Ruby っぽい</li>
<li>なんとなくC言語っぽいものをイメージして作ったが、現時点では型がない(整数しかない)ので <a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/B%E8%A8%80%E8%AA%9E">B言語</a> や <a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/BCPL">BCPL</a> の方が近いのかも(詳しくないのでボンヤリした書き方)</li>
</ul>
<p>ざっくり書くとこんな感じです。このうち「見た目は Ruby っぽい」というのがポイントで、Ruccola のパーサ( <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/ruccola/blob/00fc70c2f76e56737e290b541a69e9c36f9bb968/rcl_parser.rb">202-12-25 時点のソース</a> )にいくつか手を加えれば MinRuby のパーサとして使えそうでした。</p>
<p>コード例です。以下は Ruccola言語で書いた cat コマンド。</p>
<pre><code class="ruby">def main()
var EOF = -1;
var c;
while (true)
c = getchar();
if (c == EOF)
break;
end
write(c, 1);
end
end
</code></pre>
<p>だいたい Ruby ですね。中身は原始的なのに見た目は Ruby 風なので、書いているとちょっと不思議な気分になります。</p>
<p>作者の目にはだいたい Ruby と同じに見えますが、Ruby に慣れている方には次のような点が目に付くのではないでしょうか。</p>
<ul>
<li>文末の <code>;</code> (必須)</li>
<li>while の条件式を囲む <code>(</code> <code>)</code> (必須)</li>
<li>変数宣言の var(必須)</li>
</ul>
<p>他にも関数呼び出しの <code>(</code> <code>)</code>、関数定義の仮引数を囲む <code>(</code> <code>)</code>、エントリポイントとなる main 関数も必須です。</p>
<p>自分が言語実装ビギナーなので、難しいことをしなくていいように Ruby の文法にいろいろ制限を加えてこうなっています。Ruby と完全にコンパチではありませんが、構文ハイライトやエディタのインデント支援は Ruby 向けのものがだいたいそのまま使えています。</p>
<p>ちなみに、Ruccola よりももっとコンパクトな <strong>mini-ruccola</strong>(元は vm2gol-v2 という名前だった)もあります。VM〜コンパイラを割と素朴に書いて1500行以下というもの。自分が欲しい(難しすぎず、簡単すぎず、読み書きに慣れている言語向けの)教材が見つけられず、じゃあ自分で作ってしまえとなって作ったもの。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2">https://github.com/sonota88/vm2gol-v2</a></p>
<p>以前週刊Railsウォッチで紹介していただきました。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://techracho.bpsinc.jp/hachi8833/2021_02_09/103838">週刊Railsウォッチ(20210209後編)Rubyでミニ言語処理系を作る、Kernel#getsの意外な機能、CSSのcontent-visibilityほか|TechRacho by BPS株式会社</a></p>
<p>作った順番としては mini-ruccola の方が先で、最初にこっちで簡単なコンパイラ実装になんとか入門し、その後セルフホストに必要な機能を追加していく形で Ruccola を作っていきました。</p>
<h1 id="主な変更点"><a href="#%E4%B8%BB%E3%81%AA%E5%A4%89%E6%9B%B4%E7%82%B9">主な変更点</a></h1>
<h2 id="MinRuby の AST に合わせる"><a href="#MinRuby+%E3%81%AE+AST+%E3%81%AB%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B">MinRuby の AST に合わせる</a></h2>
<p>Ruccola の AST は現時点では MinRuby と同じく入れ子の配列(のようなもの)で木構造を表す方式です。そこは同じ。ただ細かいところが違うので合わせていきます。</p>
<p>簡単なところでいえば、たとえば以下は変数への代入です。</p>
<pre><code class="diff">- [:set, var_name, expr]
+ [:var_assign, var_name, expr]
</code></pre>
<p>ものによってはこの程度の変更でOK。</p>
<h2 id="演算子の優先順位"><a href="#%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%AE%E5%84%AA%E5%85%88%E9%A0%86%E4%BD%8D">演算子の優先順位</a></h2>
<p>Ruccola では自分でも手に負えるようにいろいろと単純化しています。演算子の優先順位もそのひとつで、たとえば <code>1 + 2 * 3</code> という式は <code>(1 + 2) * 3</code> として扱われます(単純に左結合とする)。</p>
<p>えっそれでいいの? と思いましたか? 思いますよね。</p>
<p>言語処理系の解説を見ると必ずといっていいほど演算子の優先順位の話題が出てきて、自作言語を作るには避けて通れないトピックであるかのように思われます。私もある時までそう思い込んでいたのですが、実はそんなことはありません……と思うんですよね(微妙な自信のなさ)。</p>
<p>Ruccola では <code>1 + (2 * 3)</code> と解釈させたい場合は明示的に括弧で優先順位を指定して <code>1 + (2 * 3)</code> と書けばよい、ということにしています。たまに自分でも忘れていて素でびっくりします。まあ自分の勉強用の言語だからいいんですよこれで。最初から実装しなくてもよいので後回しにしています。Ruccola言語の記述力がまだ低いため、今入れてもコード量が膨れるデメリット(セルフホストしているのでばかにならず、下手に機能追加すると自分が困る)の方が大きそうで嫌かなという考えもあり。</p>
<p>ちなみに、似たような方針を採っている言語として <a target="_blank" rel="nofollow noopener" href="http://middleriver.chagasi.com/electronics/vtl.html">VTL (Very Tiny Language)</a> という言語があると後から知りました。仲間がいた……。</p>
<blockquote>
<p>VTLでは,演算子の間に優先順位は存在せず,左から順番に演算が行われます.演算の順序を変えたい場合は括弧"()"をつけます.例えば,"2+3*4"の結果は"20"となり,"2+(3*4)"の結果は"14"となります.</p>
</blockquote>
<p>あ、ていうか優先順位を明示する言語といえば Lisp があるじゃないか、とこの記事を書いていて今思い至りました。Lisp、心強いですね。</p>
<p>さて、Ruccola ではそれでいいとして、演算子の優先順位に対応しないと cookpad-hackarade-minruby のテストが通りません。なんとかしなければ。</p>
<p>これについては自分にとって今回初めてという訳ではなく、以前単体で履修していたのでした。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/07/05/111103">四則演算と剰余のみのexprコマンドをRubyで作ってみた</a></p>
<p>これを組み込めばいけるはず。</p>
<p>test1-4.rb と test1-5.rb の間の飛躍が大きかったので小さなテストを追加しながら進めました。</p>
<ul>
<li>まずは一番優先順位の低い <code>1 == 2</code> から</li>
<li>二番目に優先順位の低い <code><</code> を右側に加えた <code>1 == 2 < 3</code></li>
<li>...</li>
<li><code>1 == 2 < 3 + 4 * 5</code> が <code>(1 == (2 < (3 + (4 * 5))))</code> と解釈できればOK</li>
<li>ここまでできたら test1-5.rb が通る</li>
</ul>
<p>という流れで進めることで意外とすんなり書き換え完了しました。</p>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html">演算子式 (Ruby 3.1 リファレンスマニュアル)</a></p>
<p>優先順位まわりについては書籍『<a target="_blank" rel="nofollow noopener" href="http://esolang-book.route477.net/">Rubyで作る奇妙なプログラミング言語</a>』もおすすめです。私はこの本の写経で入門しました。</p>
<h2 id="if と case"><a href="#if+%E3%81%A8+case">if と case</a></h2>
<p>Ruccola では if 文は case 文のシンタックスシュガーという扱いになっています。「case文が動けば if文はそのサブセット扱いにできるよね」という、大は小を兼ねる的な素朴な発想によるものです。あと Ruccola の前身の mini-ruccola は最初はパーサがなくて構文木を手書きするところからスタートしたため、その都合もあります。</p>
<p>MinRuby では逆になっていて、 case 式は内部的に入れ子の if 式としてパースされます(『RubyでつくるRuby』 p85)。ここは今回『RubyでつくるRuby』を読み返してなるほどと思ったところでした。</p>
<h2 id="改行の扱い"><a href="#%E6%94%B9%E8%A1%8C%E3%81%AE%E6%89%B1%E3%81%84">改行の扱い</a></h2>
<p>Ruccola では現時点では if文の条件式全体を囲む括弧は省略不可です(case文、while文も同様)。また、代入や関数呼び出しなどの文の末尾にはセミコロンが必須です。改行はスペースと同じように字句解析の段階で捨てています。</p>
<pre><code class="ruby">if (i == 10)
p(1);
end
</code></pre>
<p>一方 MinRuby では条件式を囲む括弧や関数呼び出しなどの末尾のセミコロンは省略可能です。改行の考慮が必要そう。</p>
<pre><code class="ruby">if i == 10
p(1)
end
</code></pre>
<p>改行を考慮したパースはこれまで経験がなく不確実な部分でしたが、結論からいえば特別な対応は不要で、適当に改行を読み飛ばすだけで cookpad-hackarade-minruby のテストは通りました(というか試しにやってみたら字句解析の段階で全部捨てても大丈夫だった)。</p>
<p>今の実装では、式とみなせるまとまりの次に二項演算子が来たら「まだ式の続きがあるな」と判断し、そうでなければ「いったんそこで式が終わっているな」と判断する、という動作になっています。<br />
たとえば上のコード例だと <code>i == 10</code> の次に <code>p</code> というトークンが来ています。これは二項演算子ではありませんから、ここで式の区切りと判断され、<code>i == 10</code> と <code>p</code> 以降は別のまとまりだということになります。</p>
<pre><code>i == 10 + ...
^ 二項演算子なのでまだ式が続いていると判断
i == 10 p ...
^ 二項演算子ではないので p からは別のまとまり
</code></pre>
<p>あくまで cookpad-hackarade-minruby のテストのカバー範囲と今回作ったパーサの実装の組み合わせでは問題なかったというだけで、Ruby に近づけていこうとするとどこかでちゃんとした対応が必要になってくるでしょうね。</p>
<h2 id="配列・ハッシュ"><a href="#%E9%85%8D%E5%88%97%E3%83%BB%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5">配列・ハッシュ</a></h2>
<p>Ruccola では配列リテラルや配列の要素にアクセスするための専用の構文は(まだ)ありません。ハッシュはサポート予定なし。</p>
<p>なので配列・ハッシュまわりは新たに書きました。ここはすんなり書けてしまって特に書くことがありません。</p>
<h2 id="TODO"><a href="#TODO">TODO</a></h2>
<ul>
<li>if, case まわりがいいかげんなのでもうちょいちゃんとやる</li>
<li>interp.rb をパースできるようにする(ここまではやりたい)</li>
</ul>
<p>(2023-01-22 追記) 上記2つ完了しました。「これで完璧!」というものではありませんが、とりあえず interp.rb はパースできています。</p>
<h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1>
<p>以上のように自作言語 Ruccola のパーサにいくつかの変更を加えることで MinRuby のパーサを作ることができました(ただし cookpad-hackarade-minruby のテストを通すところまで)。</p>
<p>以前から MinRuby のパーサ作れないかなーとボンヤリ考えていたのでこの機会に実現できてよかったです。</p>
<hr />
<p>以下、おまけ的な雑多な話題・メモなどです。</p>
<h1 id="おまけ"><a href="#%E3%81%8A%E3%81%BE%E3%81%91">おまけ</a></h1>
<h2 id="再帰下降パーサについて"><a href="#%E5%86%8D%E5%B8%B0%E4%B8%8B%E9%99%8D%E3%83%91%E3%83%BC%E3%82%B5%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">再帰下降パーサについて</a></h2>
<p>再帰下降パーサについては書籍やウェブに解説がたくさんありますので「再帰下降 パーサ」「再帰下降 構文解析」などで調べてください。英語だと recursive descent parser です。</p>
<p>今回作ったものはパーサだけで 500行くらいありますし、再帰下降パーサの一番最初の入門用としてはちょっと大きいかもしれません。そういう場合は分解してちょっとずつ手を付けるのがおすすめです。</p>
<ul>
<li>くりかえしの構造
<ul>
<li>関数定義の仮引数、関数呼び出し時の引数、演算子を含む式、配列リテラル、ハッシュリテラルなど、いろんなところで登場する</li>
<li><code>数</code> のあとに <code>, 数</code> をくりかえし</li>
<li>例: <code>1, 2, 3</code></li>
<li><code>数</code> のあとに <code>演算子 数</code> のくりかえし</li>
<li>例: <code>1 + 2 + 3</code></li>
</ul></li>
<li><code>[[]]</code>, <code>[[[]]]</code> のような入れ子の構造</li>
<li>演算子の優先順位</li>
</ul>
<p>上の方でも書いたように、演算子の優先順位は後回しにできますから、先にそれ以外の部分を作ってから後付けで追加するのもよいと思います。</p>
<p>「MinRuby のパーサを書いてみたいけどいきなり全部書くのは無理そう……また今度にしよう」と思った方は、まず次のような簡単なものから手を付けてみるのはどうでしょう。どちらも50行くらいです。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/816b6b8d7a362ef4e332">入れ子の配列だけをパースできるパーサを作る(手書きの再帰下降パーサ)</a>
<ul>
<li><code>((), (()))</code> のような入れ子の括弧だけをパースできるパーサ。コード例があった方がよさそうと思って簡単なものを昨日急いで書きました。</li>
<li>n年前の自分に「これで入門しろ」「最初はこれでいいと思うよ」といって送りつけるとしたらこれ</li>
</ul></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/5902f255e196b5b8b048">正規表現エンジン(ロブ・パイクのバックトラック実装)をRubyで写経した</a>
<ul>
<li>バックトラック方式の正規表現エンジンもおすすめです。こっちも手軽。手軽だけどおもしろい。再帰下降パーサに考え方が似ています。</li>
</ul></li>
</ul>
<h2 id="MinRuby 関連"><a href="#MinRuby+%E9%96%A2%E9%80%A3">MinRuby 関連</a></h2>
<p>今回調べていて見つけたもの。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://speakerdeck.com/m_seki/extend-your-own-programming-language-rubykaigi-2018">https://speakerdeck.com/m_seki/extend-your-own-programming-language-rubykaigi-2018</a></p>
<p>MinRuby に末尾呼び出し最適化を実装する話や Rinda + MinRuby の話。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://matsubara0507.github.io/posts/2019-05-16-minruby-with-patternmatch.html">https://matsubara0507.github.io/posts/2019-05-16-minruby-with-patternmatch.html</a></p>
<p>パーサは「木を組み立てる」処理がメインなのでパターンマッチの出番はそんなにありませんが、評価器は「木を使う」処理がメインなのでパターンマッチを使うといい感じに書けますね。</p>
<h2 id="関連書籍"><a href="#%E9%96%A2%E9%80%A3%E6%9B%B8%E7%B1%8D">関連書籍</a></h2>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="http://esolang-book.route477.net/">Rubyで作る奇妙なプログラミング言語</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://tatsu-zine.com/books/scheme-in-ruby">つくって学ぶプログラミング言語 RubyによるScheme処理系の実装 - 達人出版会</a></li>
</ul>
<p>『RubyでつくるRuby』もそうですが、どの本も古びていない、これからも通用する、使い回しのきく内容だと思います。Ruby に慣れていて言語処理系に興味があるという人におすすめです。</p>
<h2 id="mal (Make a Lisp)"><a href="#mal+%28Make+a+Lisp%29">mal (Make a Lisp)</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/kanaka/mal">https://github.com/kanaka/mal</a></p>
<p>Lisp に興味のある方には mal(Make a Lisp)もおすすめです。ちょうど『RubyでつくるRuby』の Lisp 版といった感じで、簡単な Lisp インタプリタを作ってセルフホストするまでが11ステップに分かれており、テストを通しながら作っていきます。入門者向けのシンプルな実装ですが、マクロや例外機構、末尾呼び出しの最適化まで含まれています。すでに <a target="_blank" rel="nofollow noopener" href="https://github.com/kanaka/mal/tree/03b6cfd45c99db651ece70381a7c0f596fc14336/impls/ruby">Ruby 版の実装</a>(主な部分は500行程度)もリポジトリに含まれていますから、そのまま写経してもいいですし、自力で書いて詰まったときに参考にすることもできます。</p>
<p>ちなみに私は一度 Ruby で写経した後 LibreOffice Basic で書くということをやりました。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/501f0efa437bc4d53cc7">LibreOffice BasicでLispインタプリタ(mal)を書いた - Qiita</a></p>
<h2 id="PicoRuby"><a href="#PicoRuby">PicoRuby</a></h2>
<p>入門者向けに作られているものではありませんが、Ruby 関連の小さめの処理系というと PicoRuby がありますね。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/picoruby">https://github.com/picoruby</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/picoruby/mruby-pico-compiler">https://github.com/picoruby/mruby-pico-compiler</a></p>
<p>この記事の前日(2022-12-24)に開発者募集されていました。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/picoruby/picoruby/wiki/%E3%80%90%E3%81%8A%E8%AA%98%E3%81%84%E3%80%91PicoRuby%E3%81%AE%E9%96%8B%E7%99%BA%E8%80%85%E3%82%92%E5%8B%9F%E9%9B%86%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99">【お誘い】PicoRubyの開発者を募集しています · picoruby/picoruby Wiki</a></p>
<p>Ruby よりは小さくて難しくなさそう?? 読めそうだったら参考のためにソース読んでみようかな……と思いつつまだ何もできていません。</p>
<p>Lemon というパーサジェネレータを使ってるんですね。Lemon 知らなかった。SQLite が使っているものだそうです。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://sqlite.org/lemon.html">The Lemon LALR(1) Parser Generator</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/picoruby/picoruby/wiki/Proposal-for-the-Grant-Program-2020-of-Ruby-Association">Proposal for the Grant Program 2020 of Ruby Association · picoruby/picoruby Wiki</a></li>
</ul>
<h2 id="雑多"><a href="#%E9%9B%91%E5%A4%9A">雑多</a></h2>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/97ba1f0c377fbe86d5b1">Raccでかんたんな自作言語のパーサを書いた</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/edce05e86d3248f49401">Parsletでかんたんな自作言語のパーサを書いた</a></li>
</ul>
<p>mini-ruccola のパーサを Racc と Parslet を使って書いたもの。</p>
<hr />
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/6870ef5fe5b2a9275ab7">自作言語のコンパイラにオレオレアセンブリではなくx86_64アセンブリを生成させる(関数呼び出しと足し算だけ) - Qiita</a></p>
<p>コンパイラ作ったらやはりこれもやっておきたい、ということで(ちょっとだけ)やったみたもの。RISC-V 版や WebAssembly 版もやってみたい。</p>
<hr />
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/a5d6d3539e0fb8040f74">Ruby+DXOpalでリレー式論理回路シミュレータを自作して1bit CPUまで動かした - Qiita</a></p>
<p>言語処理系をやるとさらに下のレイヤーも気になってきますよね? ということでついでに紹介。これも楽しかったです。Ruby(と <a target="_blank" rel="nofollow noopener" href="https://yhara.github.io/dxopal/doc/ja/index.html">DXOpal</a>)で作れます!</p>
<h2 id="今年 Ruby 関連で書いたもの"><a href="#%E4%BB%8A%E5%B9%B4+Ruby+%E9%96%A2%E9%80%A3%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%9F%E3%82%82%E3%81%AE">今年 Ruby 関連で書いたもの</a></h2>
<p>せっかくのアドベントカレンダーなので今年書いたものも並べてみます。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b586e96f7805a5695a34">Galaaz を触ってみた(TruffleRuby + ggplot2 で散布図を描いてみた)</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/539c631b0f7dfb295fec">Ubuntu 22.04にJupyter NotebookとIRubyをインストール(pyenv, rbenv, Bundler を使用)</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/7e2ab396163c081657b1">Ruby + Victor でSVGお絵描き(簡単な散布図を描いてみた)</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/86afece3b2595648a656">SVG::Graph(svg-graph gem)で散布図を描く</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/50609275668fcd7511e4">Ruby + ruby_gnuplot(gnuplot gem)で散布図を描く</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/da02c6f9f85c5a33f0a1">Ruby + Numo::Gnuplot(numo-gnuplot gem)で散布図を描く</a></li>
</ul>
sonota486
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
tag:crieit.net,2005:PublicArticle/17735
2021-11-03T08:04:56+09:00
2021-11-03T08:04:56+09:00
https://crieit.net/posts/simple-compiler-parser-ruby-racc
Raccでかんたんな自作言語のパーサを書いた
<p><a href="https://crieit.now.sh/upload_images/856c469c026e97152123f812f323d67e6181c1b9a3869.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/856c469c026e97152123f812f323d67e6181c1b9a3869.png?mw=700" alt="image" /></a></p>
<p><自作言語処理系の説明用テンプレ></p>
<p>自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。</p>
<ul>
<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><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>
<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>
<p><説明用テンプレおわり></p>
<hr />
<p>もともとパーサ部分は手書きの再帰下降パーサでしたが、Racc 版を作ってみました。</p>
<p>Racc で四則演算のパーサを作る方法は分かったがもう少しプログラム言語らしきものを扱っているサンプルを見たいとか、パースしたそばから実行する方式(インタプリタ方式)ではなく構文木が欲しいんだけど、という感じの人には参考になるかもしれません(昔の自分に送ってあげたい)。</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_racc.y</code> を追加したブランチです。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/tree/alt_parser_racc">github.com/sonota88/vm2gol-v2/tree/alt_parser_racc</a></p>
<p>300行弱なので全部貼ってもいいのですが、雰囲気程度ということで途中を省略したものを貼ります。全体は GitHub の方で見てください。</p>
<pre><code class="ruby">class Parser
prechigh
left "+" "*"
preclow
rule
program:
top_stmts
{
top_stmts = val[0]
result = ["top_stmts", *top_stmts]
}
top_stmts:
top_stmt
{
top_stmt = val[0]
result = [top_stmt]
}
| top_stmts top_stmt
{
top_stmts, top_stmt = val
result = [*top_stmts, top_stmt]
}
top_stmt:
func_def
func_def:
"func" IDENT "(" args ")" "{" stmts "}"
{
_, fn_name, _, args, _, _, stmts, _, = val
result = ["func", fn_name, args, stmts]
}
args:
# nothing
{
result = []
}
| arg
{
arg = val[0]
result = [arg]
}
| args "," arg
{
args, _, arg = val
result = [*args, arg]
}
arg:
IDENT | INT
stmts:
# nothing
{
result = []
}
| stmt
{
stmt = val[0]
result = [stmt]
}
| stmts stmt
{
stmts, stmt = val
result = [*stmts, stmt]
}
stmt:
stmt_var
| stmt_set
| stmt_return
| stmt_call
| stmt_call_set
| stmt_while
| stmt_case
| stmt_vm_comment
| stmt_debug
stmt_var:
"var" IDENT ";"
{
_, ident, _ = val
result = ["var", ident]
}
| "var" IDENT "=" expr ";"
{
_, ident, _, expr = val
result = ["var", ident, expr]
}
# ... 途中省略 ...
---- header
require "json"
require_relative "common"
---- inner
def next_token
@tokens.shift
end
def to_token(line)
token = Token.from_line(line)
return nil if token.nil?
if token.kind == :int
Token.new(token.kind, token.value.to_i)
else
token
end
end
def read_tokens(src)
tokens = []
src.each_line do |line|
token = to_token(line)
next if token.nil?
tokens << token
end
tokens
end
def to_racc_token(token)
kind =
case token.kind
when :ident then :IDENT
when :int then :INT
when :str then :STR
else
token.value
end
[kind, token.value]
end
def parse(src)
tokens = read_tokens(src)
@tokens = tokens.map { |token| to_racc_token(token) }
@tokens << [false, false]
do_parse()
end
---- footer
if $0 == __FILE__
ast = Parser.new.parse(ARGF.read)
puts JSON.pretty_generate(ast)
end
</code></pre>
<h1 id="実行の例"><a href="#%E5%AE%9F%E8%A1%8C%E3%81%AE%E4%BE%8B">実行の例</a></h1>
<p>入力とするプログラムの例です。</p>
<pre><code class="javascript">// sample.vg.txt
func main() {
return 1 + (2 * 3);
}
</code></pre>
<p>次のように実行するとASTに変換できます。</p>
<pre><code class="sh">## vgparser_racc.rb を生成
$ bundle exec racc -t -o vgparser_racc.rb vgparser_racc.y
$ ruby vglexer.rb sample.vg.txt > tmp/tokens.txt
$ ruby vgparser_racc.rb tmp/tokens.txt
[
"top_stmts",
[
"func",
"main",
[
],
[
[
"return",
[
"+",
1,
[
"*",
2,
3
]
]
]
]
]
]
</code></pre>
<h1 id="スタックの動きを見てみる"><a href="#%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E5%8B%95%E3%81%8D%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%BF%E3%82%8B">スタックの動きを見てみる</a></h1>
<p>せっかく Racc を使っているので、適当なサンプルコード(下記)をパースさせてスタックの動きを図にしてみました。</p>
<pre><code class="javascript">func add(a, b) {
var c = a + b;
return c;
}
func main() {
var x = -1;
var i = 0;
while (i != 10) {
case when (i == 1) {
call_set x = add(i, x);
} when (i == 2) {
set x = 1 + 2;
} when (1) {
set x = 1 + (2 * 3);
}
set i = i + 1;
}
}
</code></pre>
<p>図の描き方については <a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/6a96d2bcea9d134e38b7">Ruby/Racc: パース時のスタックの動きをFlameGraphっぽくビジュアライズする</a> を参照。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/66f17460-1b71-e4bc-f82a-7a48d3fa269c.png" alt="image.png" /></p>
<p>左端の、全体の 1/5 くらいの小さめの山が <code>add</code> 関数で、残りが <code>main</code> 関数ですね。</p>
<h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1>
<ul>
<li>レキサはすでにあるのでそれを流用した
<ul>
<li><code>to_racc_token</code> メソッドで Token オブジェクトを Racc が期待する形式に変換</li>
</ul></li>
<li>1時間くらいで書けた。これより大きめの SQL パーサを少し前にすでに書いていてある程度慣れていたのと、パーサのテストがそのまま使えたのが良かった。</li>
</ul>
<h1 id="関連"><a href="#%E9%96%A2%E9%80%A3">関連</a></h1>
<p>他に Racc 関連で書いたもの。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/6a96d2bcea9d134e38b7">Ruby/Racc: パース時のスタックの動きをFlameGraphっぽくビジュアライズする</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/f55c1654fe101fa0e557">Ruby/Racc: パースに失敗した位置(行、桁)を得る</a></li>
</ul>
<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/edce05e86d3248f49401">Parsletでかんたんな自作言語のパーサを書いた</a></li>
</ul>
sonota486
tag:crieit.net,2005:PublicArticle/17398
2021-06-14T06:43:44+09:00
2021-06-15T07:18:25+09:00
https://crieit.net/posts/ruby-simple-compiler-vm2gol-v2
Rubyでかんたんな自作言語のコンパイラを作った
<p>※ <a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/2b95378b43a22109513c">Qiitaに書いた記事</a>のクロス投稿です。</p>
<p><a href="https://crieit.now.sh/upload_images/696f2eb5274806d16114ce8b256cda6260c67b4de7922.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/696f2eb5274806d16114ce8b256cda6260c67b4de7922.png?mw=700" alt="image" /></a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/f9cb3fc4a496b354b729">RubyでオレオレVMとアセンブラとコード生成器を2週間で作ってライフゲームを動かした話</a>の続きです。このときスコープ外にしていたフロントエンド部分(高水準言語から構文木への変換部分)をやっと書きました。3日くらいでできたのでさっさとやっておけばよかった。</p>
<p>パーサまで揃っていなかったため仕方なく「コード生成器を作った」という表現にしていましたが、ここまでやったら「コンパイラを作った」と言っていいはず!</p>
<p>※ <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/05/04/155425">ブログに書いていたもの</a>を引っ越してきました。元の記事公開日は 2020-05-04 です。</p>
<hr />
<ul>
<li>パーサ
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/blob/41/vgparser.rb">https://github.com/sonota88/vm2gol-v2/blob/41/vgparser.rb</a></li>
</ul></li>
<li>ライフゲームのコード
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/blob/41/gol.vg.txt">https://github.com/sonota88/vm2gol-v2/blob/41/gol.vg.txt</a></li>
</ul></li>
</ul>
<p>※ 2020-05-04 時点のものです。その後の改良も加わったものが見たい場合は <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2">mainブランチ</a> を見てください。<br />
※ パーサ以外の部分をどうやって作ったか知りたい方は<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2019/05/04/234516">こちら</a>をどうぞ<br />
※ 他の言語にも移植していますので Ruby わからんという方は<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/132314">こちら</a>もどうぞ(2021-06-15 現在での移植先: TypeScript, Python, Dart, Java, C, Perl, C♭, PHP, Go, LibreOffice Basic, Zig, Kotlin, Pric, Crystal, Rust, Julia, Pascal)</p>
<hr />
<ul>
<li>ふつうの手書き再帰下降パーサ
<ul>
<li>400行ちょっと</li>
</ul></li>
<li>難しいことはしない
<ul>
<li>再帰下降パーサを実際に書くのは今回が初めてなので</li>
<li>まずはハードルを下げて「とにかく動く」ところまで持って行く
<ul>
<li>下げ過ぎた気がしなくもない</li>
</ul></li>
<li>構文木を割とそのままマッピングしていて、気の利いた変換とかはやってない</li>
<li>式の優先順位は考慮しない
<ul>
<li>明示的に括弧を書く</li>
<li>気が向いたら後でやる</li>
</ul></li>
</ul></li>
<li>ベタな文法でよい / 高度な機能や独自色をなるべく入れないようにしたい
<ul>
<li>初心者向け教材っぽくしておきたい気持ち</li>
</ul></li>
<li>汎用ではない
<ul>
<li>ライフゲームだけコンパイルできればよい</li>
</ul></li>
<li>エラーハンドリングは適当
<ul>
<li>ライフゲームだけコンパイルできればよい</li>
</ul></li>
<li>ふつうの文法でふつうの再帰下降パーサなので、あまり書くことがないです……</li>
</ul>
<h1 id="文法サンプル"><a href="#%E6%96%87%E6%B3%95%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB">文法サンプル</a></h1>
<p>見ての通り、特にひねりのない感じです。パッと見では JavaScript に近いでしょうか。</p>
<p><code>set</code>, <code>call</code>, <code>call_set</code> は明示的に書きます。</p>
<pre><code class="javascript">// コメント
// 関数定義
func add(a, b) {
// return(返り値は省略可)
return a + b;
}
// main 関数は必須
func main() {
// ローカル変数宣言
var a;
// ローカル変数宣言+初期化
var b = 1;
// ローカル変数への代入
set a = 2;
// 関数呼び出し
call add(1, 2);
// 関数呼び出して返り値をローカル変数に代入
call_set c = add(1, 2);
// while
while (a != 10) {
// ...
}
// case
case {
(a == 1) {
// ...
}
(a == 2) {
// ...
}
(0 == 0) { // else の代わり
// ...
}
}
// VMコメント
_cmt("...");
}
</code></pre>
<h1 id="コンパイルからVMでの実行までの流れ"><a href="#%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB%E3%81%8B%E3%82%89VM%E3%81%A7%E3%81%AE%E5%AE%9F%E8%A1%8C%E3%81%BE%E3%81%A7%E3%81%AE%E6%B5%81%E3%82%8C">コンパイルからVMでの実行までの流れ</a></h1>
<p>足し算をする関数を呼び出して結果を受け取るだけのサンプルで高水準言語から機械語までの変換の流れを具体的に見てみます。</p>
<hr />
<p>高水準言語:</p>
<pre><code class="javascript">func add(a, b) {
var result = a + b;
return result;
}
func main() {
var result;
call_set result = add(1, 2);
}
</code></pre>
<p>↓ <code>ruby vgparser.rb add.vg.txt > add.vgt.json</code> で AST に変換(実際の出力は改行が多くて冗長なので下記は手動で整形しています)</p>
<pre><code class="json">[
"stmts",
[
"func", "add", ["a", "b"],
[
["var", "result", ["+", "a", "b"]],
["return", "result"]
]
],
[
"func", "main", [],
[
["var", "result"],
["call_set", "result", ["add", 1, 2]]
]
]
]
</code></pre>
<p>↓ <code>ruby vgcg.rb add.vgt.json > add.vga.txt</code> でアセンブリコードに変換</p>
<pre><code class="sh"> call main
exit
label add
push bp
cp sp bp
# 関数の処理本体
sub_sp 1
push [bp+2]
push [bp+3]
pop reg_b
pop reg_a
add_ab
cp reg_a [bp-1]
cp [bp-1] reg_a
cp bp sp
pop bp
ret
label main
push bp
cp sp bp
# 関数の処理本体
sub_sp 1
push 2
push 1
_cmt call_set<del>add
call add
add_sp 2
cp reg_a [bp-1]
cp bp sp
pop bp
ret
</code></pre>
<p>↓ <code>ruby vgasm.rb add.vga.txt > add.vge.yaml</code> で機械語コードに変換</p>
<pre><code class="yaml">---
- call
- 35
- exit
- label
- add
- push
- bp
- cp
- sp
- bp
- sub_sp
- 1
- push
- "[bp+2]"
- push
- "[bp+3]"
- pop
- reg_b
- pop
- reg_a
- add_ab
- cp
- reg_a
- "[bp-1]"
- cp
- "[bp-1]"
- reg_a
- cp
- bp
- sp
- pop
- bp
- ret
- label
- main
- push
- bp
- cp
- sp
- bp
- sub_sp
- 1
- push
- 2
- push
- 1
- _cmt
- call_set</del>add
- call
- 5
- add_sp
- 2
- cp
- reg_a
- "[bp-1]"
- cp
- bp
- sp
- pop
- bp
- ret
</code></pre>
<p>(2021-01-11 追記)<br />
この機械語コードのフォーマットは1命令1行となるようにその後変更しました: <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2021/01/10/124904">vm2gol v2 (51) 機械語コードのフォーマットを固定長風に変更</a><br />
(追記ここまで)</p>
<p>あとは <code>STEP= ruby vgvm.rb add.vge.yaml</code> とすると VM で1命令ずつステップ実行できます。</p>
<p>また、これまでと同様に <code>run.sh</code> を使って <code>./run.sh gol.vg.txt</code> のように実行すると<br />
パース → コード生成 → アセンブル → VMで実行<br />
がまとめて実行できます。</p>
<h1 id="2020-06-25 追記"><a href="#2020-06-25+%E8%BF%BD%E8%A8%98">2020-06-25 追記</a></h1>
<p>節目っぽいのでこの時点での行数を数えてみました。空行やコメントだけの行も含めた単純な行数です。</p>
<pre><code> 14 common.rb
63 vgasm.rb
474 vgcg.rb
433 vgparser.rb
491 vgvm.rb
1475 合計
</code></pre>
<p>思ったより少ない。2,000行超えてるかなと思ってましたが。</p>
<p><code>gol.vg.txt</code> と、コンパイル・アセンブルで生成される AST・アセンブリコード・機械語コードも行数を見てみます。</p>
<pre><code> 208 gol.vg.txt
874 tmp/gol.vgt.json
693 tmp/gol.vga.txt
1299 tmp/gol.vge.yaml
</code></pre>
<p>フーン、という感じですね(気の利いた感想が出てこなかった)。</p>
<h1 id="その後の変更"><a href="#%E3%81%9D%E3%81%AE%E5%BE%8C%E3%81%AE%E5%A4%89%E6%9B%B4">その後の変更</a></h1>
<p>けっこういいかげんなところがあるので後から修正しています。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/23/154759">vm2gol v2 (47) 引数のパースの厳密化など</a></li>
</ul>
<hr />
<p>(2021-02-21 追記)いくつか機能を足してセルフホストできるようになりました。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/pric">https://github.com/sonota88/pric</a></li>
</ul>
<h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://b.hatena.ne.jp/sonota88/Parser/">Parserに関するsonota88のブックマーク - はてなブックマーク</a></li>
</ul>
<h1 id="関連"><a href="#%E9%96%A2%E9%80%A3">関連</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/132314">vm2gol-v2 移植まとめ</a>
<ul>
<li>いろんな言語に移植してみています</li>
</ul></li>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/07/05/111103">四則演算と剰余のみのexprコマンドをRubyで作ってみた</a>
<ul>
<li>演算子の優先順位の考慮も必要な場合はこの方法で。<br />
ただ、ライフゲームを動かす分にはあんまり必要ないんですよね……。</li>
</ul></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/a5d6d3539e0fb8040f74">リレー式論理回路シミュレータを自作して1bit CPUまで動かした</a>
<ul>
<li>もっと低いレイヤーにも手を出してみました</li>
</ul></li>
</ul>
<hr />
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2019/05/04/234516">目次ページに戻る</a> / <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2019/12/21/144455">前</a> / <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/06/20/083831">次</a></li>
</ul>
sonota486
tag:crieit.net,2005:PublicArticle/16575
2021-01-10T08:38:01+09:00
2021-01-10T09:43:15+09:00
https://crieit.net/posts/Ruby-Racc-FlameGraph-5ffa3e59ccb77
Ruby/Racc: パース時のスタックの動きをFlameGraphっぽくビジュアライズする
<p>(少し前に同じ記事を投稿していましたが、すぐに次の記事を投稿するとモバイル版では一覧で(折りたたまれて)見えなくなるようだったので一度引っ込めて投稿し直しました。まだ Crieit に慣れてないのでご容赦くださいませ……)</p>
<p><a href="https://crieit.now.sh/upload_images/0e1305922bed9360c37ac2ff5dc93c965ff91788b6896.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0e1305922bed9360c37ac2ff5dc93c965ff91788b6896.png?mw=700" alt="image" /></a></p>
<p><a href="https://crieit.now.sh/upload_images/8872ba6bd00c951d584f5e0daae90a325ff917c1c163e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8872ba6bd00c951d584f5e0daae90a325ff917c1c163e.png?mw=700" alt="image" /></a></p>
<p>(↓これは FlameGraph に描かせたもの)<br />
<a href="https://crieit.now.sh/upload_images/e6294cd60200a969e3a5fac9581380e25ff917fa962f1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e6294cd60200a969e3a5fac9581380e25ff917fa962f1.png?mw=700" alt="image" /></a></p>
<p>こういう図を描いてみました。</p>
<h1 id="手順"><a href="#%E6%89%8B%E9%A0%86">手順</a></h1>
<h2 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h2>
<p>説明用の簡単なサンプルです。<code>1 + 2</code> のような入力を受理するパーサ。</p>
<p>ログ出力のために <code>@yydebug</code>, <code>@racc_debug_out</code> を設定しておきます。</p>
<pre><code class="ruby"># sample_add.y
class Parser
rule
expr: INT "+" INT
{
puts "expression found"
result = val
}
end
---- inner
def initialize
# parser.rb を使ったときにデバッグ情報を出力する
@yydebug = true
# デバッグ情報の出力先をファイルに変更(デフォルトでは標準エラー出力) …… これは必須ではない
@racc_debug_out = File.open("debug.log", "wb")
end
def next_token
@tokens.shift
end
def parse(src)
@tokens = src.split(" ").map do |s|
case s
when /^\d+$/ then [:INT, s.to_i]
else [s, s]
end
end
@tokens << [false, false]
do_parse
end
---- footer
src = ARGV[0]
result = Parser.new().parse(src)
puts "result: " + result.inspect
</code></pre>
<p><code>-t (--debug)</code> オプションを付けて racc コマンドを実行。</p>
<pre><code class="bash"># パーサを生成
racc -t -o parser.rb sample_add.y
</code></pre>
<p>生成されたパーサを実行すると次のような内容が debug.log に出力されます。</p>
<pre><code class="bash">$ ruby parser.rb "1 + 2"
expression found
result: [1, "+", 2]
$ head -30 debug.log
read :INT(INT) 1
shift INT
[ (INT 1) ]
goto 2
[ 0 2 ]
read "+"("+") "+"
shift "+"
[ (INT 1) ("+" "+") ]
goto 4
[ 0 2 4 ]
read :INT(INT) 2
shift INT
[ (INT 1) ("+" "+") (INT 2) ]
goto 6
[ 0 2 4 6 ]
reduce INT "+" INT --> expr
[ (expr [1, "+", 2]) ]
goto 1
[ 0 1 ]
</code></pre>
<p>スタックの部分だけ欲しいので、 grep してみましょうか。</p>
<pre><code class="bash">$ grep '\[ (' debug.log
[ (INT 1) ]
[ (INT 1) ("+" "+") ]
[ (INT 1) ("+" "+") (INT 2) ]
[ (expr [1, "+", 2]) ]
[ (expr [1, "+", 2]) ($end false) ]
[ (expr [1, "+", 2]) ($end false) ($end false) ]
</code></pre>
<p>軽く眺める程度ならこれだけでいいかもしれませんね。で、もっと複雑になったときにきれいに表示して見たい……ということで次へ。</p>
<h2 id="パースしやすいログを出力する"><a href="#%E3%83%91%E3%83%BC%E3%82%B9%E3%81%97%E3%82%84%E3%81%99%E3%81%84%E3%83%AD%E3%82%B0%E3%82%92%E5%87%BA%E5%8A%9B%E3%81%99%E3%82%8B">パースしやすいログを出力する</a></h2>
<p>ログに出力されたスタックの情報を使いたいのですが、そのままではパースしづらそうです。まじめにやろうとすると、これ用のパーサが必要になってしまいます(上の例のような簡単なものであればそんなに難しくなさそうですが)。</p>
<p>そこで、横着して最初からパースしやすいフォーマットで出力することにしました。</p>
<p>スタックの情報をどこで出力しているか調べると、ここ(<code>Racc::Parser#racc_print_stacks</code>)です。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/ruby/racc/blob/v1.5.2/lib/racc/parser.rb#L604-L611">https://github.com/ruby/racc/blob/v1.5.2/lib/racc/parser.rb#L604-L611</a><br />
嬉しいことに単独のメソッドになっています。</p>
<p>※ ちなみに呼び出し元を追っていくと分かりますが、スタックの実体は<br />
<code>Racc::Parser#racc_tstack</code><br />
<code>Racc::Parser#racc_vstack</code><br />
です(それぞれ記号と値のスタック)。</p>
<p>racc コマンドの出力は <code>Racc::Parser</code> を継承したクラスになるので、<code>parser.y</code> の inner セクションに <code>racc_print_stacks</code> メソッドを書いておくと動作をオーバーライドできます。</p>
<p>……というわけで、下記のようにしました。スタックの情報だけ JSON で別ファイルに出力します。</p>
<pre><code class="ruby">---- header
require "json"
---- inner
def initialize
# ...
@racc_stack_out = File.open("stack.log", "wb")
end
# Override Racc::Parser#racc_print_stacks
def racc_print_stacks(tstack, vstack)
super(tstack, vstack)
stack = tstack.zip(vstack).map { |t, v| [racc_token2str(t), v] }
@racc_stack_out.puts JSON.generate(stack)
end
</code></pre>
<pre><code class="bash">$ ruby parser.rb "1 + 2"
expression found
result: [1, "+", 2]
$ cat stack.log
[["INT",1]]
[["INT",1],["\"+\"","+"]]
[["INT",1],["\"+\"","+"],["INT",2]]
[["expr",[1,"+",2]]]
[["expr",[1,"+",2]],["$end",false]]
[["expr",[1,"+",2]],["$end",false],["$end",false]]
</code></pre>
<p>これでパースしやすくなりました 👌</p>
<h2 id="仕込み部分のまとめ"><a href="#%E4%BB%95%E8%BE%BC%E3%81%BF%E9%83%A8%E5%88%86%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81">仕込み部分のまとめ</a></h2>
<p>ここまでのポイントをまとめておきます。</p>
<ul>
<li><code>@yydebug = true</code></li>
<li><code>@racc_debug_out</code> を設定
<ul>
<li>これは必須ではないが、標準エラー出力に出てほしくなければファイルなど別の出力先を設定しておく</li>
</ul></li>
<li><code>Racc::Parser#racc_print_stacks</code> をオーバーライド</li>
<li>racc コマンドに <code>-t (--debug)</code> オプションを付けてパーサを生成</li>
</ul>
<h2 id="図に変換する"><a href="#%E5%9B%B3%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B">図に変換する</a></h2>
<p>ここまでできたら、あとは stack.log を図に変換するだけ。</p>
<pre><code class="bash">ruby stack_graph.rb stack.log > stack_graph.html
</code></pre>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/racc-stack-graph/blob/main/stack_graph.rb">stack_graph.rb</a> は 160行くらい(2021-01-04 時点)の簡単なスクリプトです。</p>
<p>出力された HTML をブラウザで開くとこういう図が表示されます。x軸が処理の経過の方向、y軸がスタックが伸びる方向です。</p>
<p><a href="https://crieit.now.sh/upload_images/0e1305922bed9360c37ac2ff5dc93c965ff918285b7f4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0e1305922bed9360c37ac2ff5dc93c965ff918285b7f4.png?mw=700" alt="image" /></a></p>
<p>※ この例では、 <code>INT</code> や <code>expr</code> など定型的なものはパレット指定、それ以外はランダムに色を決めています。</p>
<hr />
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/racc-stack-graph">https://github.com/sonota88/racc-stack-graph</a></p>
<p>このリポジトリを git clone して <code>./run.sh sample_add.y "1 + 2"</code> で試せます(パーサの生成 → パーサの実行 → 図の生成をまとめて実行)。</p>
<h1 id="例1: 左結合・右結合の違いを見てみる"><a href="#%E4%BE%8B1%3A+%E5%B7%A6%E7%B5%90%E5%90%88%E3%83%BB%E5%8F%B3%E7%B5%90%E5%90%88%E3%81%AE%E9%81%95%E3%81%84%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%BF%E3%82%8B">例1: 左結合・右結合の違いを見てみる</a></h1>
<p>せっかくなので他の例も見てみましょう。</p>
<p>まずは左結合と右結合の違い。他の部分は同じなので <code>class Parser ... end</code> の部分だけ示します。</p>
<pre><code class="ruby"># sample_lr.y
class Parser
prechigh
left "+"
# right "+"
preclow
rule
program: expr
{
puts "program found"
result = val[0]
}
expr:
INT
| expr "+" expr { result = val }
end
</code></pre>
<pre><code class="bash">./run.sh sample_lr.y "1 + 2 + 3"
</code></pre>
<p><a href="https://crieit.now.sh/upload_images/9e4478b04a779832b64ec14dbca107fa5ff91857c4753.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/9e4478b04a779832b64ec14dbca107fa5ff91857c4753.png?mw=700" alt="image" /></a></p>
<p><code>1 + 2</code> が来た時点ですぐ還元(reduce)され、次に <code>expr + 3</code> となったときにまた還元されています。</p>
<hr />
<p>右結合に変えてみるとこう。パースの結果が <code>[1, "+", [2, "+", 3]]</code> になります。</p>
<p><a href="https://crieit.now.sh/upload_images/709009c6f1e1ae5c0851581a79578a7d5ff91875cf671.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/709009c6f1e1ae5c0851581a79578a7d5ff91875cf671.png?mw=700" alt="image" /></a></p>
<p>最初の <code>1 + 2</code> までシフト(shift)した時点では <code>1 + 2</code> の還元は発生せず、さらにスタックが積み上がったところで <code>2 + 3</code> が還元され、その後で <code>1 + expr</code> が還元されています。</p>
<h1 id="例2: SQL"><a href="#%E4%BE%8B2%3A+SQL">例2: SQL</a></h1>
<p><a href="https://crieit.now.sh/upload_images/861923e32cdc780d5e38bd128b69907a5ff918970228e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/861923e32cdc780d5e38bd128b69907a5ff918970228e.png?mw=700" alt="image" /></a></p>
<p>もう少し大きめの実際的なものということで、別件で作っているSQLパーサの例。select句、from句、…… と順番に還元され、最後に全体が1個の select文に還元されている様子です。</p>
<p>次のような入力を与えました。動作確認用なので内容は適当。</p>
<pre><code class="sql">select
123, 'str', null
,t1.a
,max(b)
,(
case
when a = 1 then 2
else 3
end
) as foo
from
db1 . table1 as t1
left outer join db2 . table2 as t2
on (
t2 . a = t1 . a
and t2 . b = t1 . b
)
and t2.a = t1.a
where
a = 123
and b <> 456
and c in (1, 2)
group by a, b
order by a, b
limit 10
;
</code></pre>
<h1 id="FlameGraph に渡してみる"><a href="#FlameGraph+%E3%81%AB%E6%B8%A1%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">FlameGraph に渡してみる</a></h1>
<p>stack.log をちょっと加工すると <a target="_blank" rel="nofollow noopener" href="https://github.com/brendangregg/FlameGraph">FlameGraph</a> に渡せるのでは? と後から気づいてやってみました(この節は後から追記しました)。</p>
<pre><code class="ruby"># to_flamegraph.rb
require "json"
File.open(ARGV[0]).each_line do |line|
labels = JSON.parse(line).map { |t, _| t.to_s.gsub(";", "(semicolon)") }
puts labels.join(";") + " 1"
end
</code></pre>
<pre><code class="bash">ruby to_flamegraph.rb stack.log | path/to/flamegraph.pl > flamegraph.svg
</code></pre>
<p><a href="https://crieit.now.sh/upload_images/5cd6c4261e93c57cbbd204b6ef818ded5ff918c088109.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/5cd6c4261e93c57cbbd204b6ef818ded5ff918c088109.png?mw=700" alt="image" /></a></p>
<p><a href="https://crieit.now.sh/upload_images/e6294cd60200a969e3a5fac9581380e25ff918cb98474.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e6294cd60200a969e3a5fac9581380e25ff918cb98474.png?mw=700" alt="image" /></a></p>
<p>できますね 😃</p>
<p>FlameGraph で生成した SVG だと、キーワードにマッチする部分のハイライトや一部分のズーム表示といったインタラクティブな機能が利用できて便利。</p>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://yuroyoro.hatenablog.com/entry/2017/11/09/124805">ディスク使用量をFlameGraphで可視化する - ( ꒪⌓꒪) ゆるよろ日記</a></p>
<h1 id="バージョン"><a href="#%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3">バージョン</a></h1>
<pre><code>racc 1.5.2
</code></pre>
<h1 id="関連"><a href="#%E9%96%A2%E9%80%A3">関連</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/10/31/113917">Ruby/Racc: パースに失敗した位置(行、桁)を得る</a></li>
</ul>
<p>以下は Racc 関連ではありませんが、Ruby + パーサ関連で書いたものということで。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/05/04/155425">Rubyで素朴な自作言語のコンパイラを作った</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/07/05/111103">四則演算と剰余のみのexprコマンドをRubyで作ってみた</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/07/23/090115">正規表現エンジン(ロブ・パイクのバックトラック実装)をRubyで写経した</a></li>
</ul>
sonota486