tag:crieit.net,2005:https://crieit.net/users/sonota88/feed sonota486の投稿 - Crieit Crieitでユーザーsonota486による最近の投稿 2023-12-06T18:36:02+09:00 https://crieit.net/users/sonota88/feed tag:crieit.net,2005:PublicArticle/18672 2023-12-06T18:31:09+09:00 2023-12-06T18:36:02+09:00 https://crieit.net/posts/DXOpal DXOpalでスイカゲームみたいなのを作った <p>これは <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2023/ruby">Ruby Advent Calendar 2023</a> の6日目の記事です。一つ前の記事は<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/ryutaromizokami/articles/333ef2dd6d9c51">Rubyのプロダクトコードの複雑度の変化を可視化する</a>です。</p> <p><a href="https://crieit.now.sh/upload_images/8df96ea7f5523b51dd0222ad1ab4a57e65703da03c379.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8df96ea7f5523b51dd0222ad1ab4a57e65703da03c379.png?mw=700" alt="image" /></a></p> <p>こんにちは。今年は mruby や picoruby にも触れてみたかったのですがもう12月ですね……おかしいな……。</p> <h1 id="できたもの"><a href="#%E3%81%A7%E3%81%8D%E3%81%9F%E3%82%82%E3%81%AE">できたもの</a></h1> <p>遊んでみたい方はこちらのリンクからどうぞ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://sonota88.github.io/dxopal-suikalike/index.html">https://sonota88.github.io/dxopal-suikalike/index.html</a></p> <ul> <li>マウス操作のみ対応しています</li> <li>ボール(正方形ですが)のサイズは 1 から 10 まで <ul> <li>球にするのが面倒だったため正方形で作り始め、面倒だったため正方形のまま完成としました。以下「ボール」と言い張ります。</li> </ul></li> <li>オリジナルに寄せる方向ではがんばってません。それっぽければヨシ。</li> </ul> <hr /> <p>リポジトリはこちら。効果音はパブリックドメインとします。何かに使えそうなら使ってください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/dxopal-suikalike">https://github.com/sonota88/dxopal-suikalike</a></p> <h1 id="DXOpal + 物理演算 → スイカゲーム"><a href="#DXOpal+%2B+%E7%89%A9%E7%90%86%E6%BC%94%E7%AE%97+%E2%86%92+%E3%82%B9%E3%82%A4%E3%82%AB%E3%82%B2%E3%83%BC%E3%83%A0">DXOpal + 物理演算 → スイカゲーム</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/yhara/dxopal">https://github.com/yhara/dxopal</a></p> <p>DXOpal についてざっくり特徴を挙げると次のような感じでしょうか。</p> <ul> <li>手軽にゲームを作れるフレームワーク</li> <li>作ったゲームはブラウザで実行する <ul> <li>ここで <a target="_blank" rel="nofollow noopener" href="https://opalrb.com/">Opal</a> が使われる(Ruby のコードが JavaScript に変換されて実行される)</li> <li>OS などの環境に依存せず同じように動かせる</li> <li>ウェブで配信する形にすればダウンロードやインストールなしで使ってもらえる・遊んでもらえる・見てもらえる</li> <li>参考: <a target="_blank" rel="nofollow noopener" href="https://qiita.com/yhara/items/24141953af652ad96db0">DXOpalで作ったゲームを無料で公開する</a></li> </ul></li> </ul> <p>入門用のドキュメントとしては以下2つがおすすめです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://magazine.rubyist.net/articles/0057/0057-GameProgramingWithDXOpal.html">Rubyで始めるゲームプログラミング - DXOpal編 -</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2018/opal">Opal Advent Calendar 2018</a> の「DXOpalでUndertaleっぽい画面を作る」シリーズ</li> </ul> <hr /> <p>DXOpal には物理演算ライブラリ(<a target="_blank" rel="nofollow noopener" href="https://brm.io/matter-js/">matter.js</a>)を利用した機能が実験的に用意されており、リポジトリの examples ディレクトリにサンプルがあります。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/yhara/dxopal/tree/master/examples/matter">https://github.com/yhara/dxopal/tree/master/examples/matter</a></p> <p>これを使って何かできないかなーと以前からボンヤリ考えていたのですが、今年はスイカゲームが流行りましたから、じゃあ作ろうかと。</p> <p>作り自体は割と簡単です。めんどくさいところ(ボール同士の接触判定や物理演算)は DXOpal と matter.js がやってくれるので基本部分は数時間で動くようになりました。</p> <p>できあがりのサイズはこのくらい。小さく作るのが好き。</p> <pre><code> $ wc -l *.rb 57 ball.rb 314 game.rb 119 main.rb 490 合計 </code></pre> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <p>以下、開発メモです。</p> <h2 id="物体の消去"><a href="#%E7%89%A9%E4%BD%93%E3%81%AE%E6%B6%88%E5%8E%BB">物体の消去</a></h2> <p>なるべく DXOpal 本体に手を加えない範囲に収めたかったのですが、物体を消去する処理は必要なのでここだけ追加しました。</p> <pre><code class="diff">--- a/lib/dxopal/sprite/physics.rb +++ b/lib/dxopal/sprite/physics.rb @@ -56,6 +56,15 @@ module DXOpal `Matter.World.addBody(#{Sprite._matter_engine}.world, body)` end + def remove_matter_body + Sprite._remove_matter_body(@_matter_body) + end + + def self._remove_matter_body(body) + _matter_sprites.delete(`body.id`) + `Matter.World.remove(#{Sprite._matter_engine}.world, body, null)` + end + # Return true if `physical_body=` is ever called def self.matter_enabled? # Note: we cannot use `!!` here because @matter_engine may be a JS object, </code></pre> <p>これを追加するために matter.js のドキュメントとソースを軽く眺めましたが、matter.js 絡みの手間はその程度で済みました。見様見真似の間に合わせなのでひょっとしたらおかしなことしてるかもしれません。</p> <h2 id="音量の変更"><a href="#%E9%9F%B3%E9%87%8F%E3%81%AE%E5%A4%89%E6%9B%B4">音量の変更</a></h2> <p>やっぱり音量調節もしたいということで。Sound クラスと SoundEffect クラスの代わりに少しいじったクラスを用意してそっちを使っています。DXOpal 本体に手を入れなくて済むので今回はこの方式にしました。</p> <h2 id="事前コンパイル"><a href="#%E4%BA%8B%E5%89%8D%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB">事前コンパイル</a></h2> <p>今回は事前コンパイルする方式で開発を進めました。以下の記事を参照してください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/536c76d7383356592b92">Dockerを利用してDXOpalアプリケーションを手軽に事前コンパイルする</a></p> <h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1> <ul> <li>DXOpal を使うとブラウザで動かせるゲームが手軽に作れて便利</li> <li>スイカゲームみたいなのを作った</li> </ul> <p>以下おまけです。</p> <h1 id="dxopal-demos"><a href="#dxopal-demos">dxopal-demos</a></h1> <p>今回作ったものとは無関係ですが DXOpal 関連ということでご紹介です。<br /> 思いつきで書いてみた小物をときどき追加しているリポジトリです。アルゴリズムの解説とか見るとこういうの作りたくなりますよね。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/dxopal-demos">https://github.com/sonota88/dxopal-demos</a></p> <hr /> <p>メタボール</p> <p><a href="https://crieit.now.sh/upload_images/90326c63d6ab5f7ad23a2f3fca2cf4dc65703db8a0b5a.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/90326c63d6ab5f7ad23a2f3fca2cf4dc65703db8a0b5a.gif?mw=700" alt="image" /></a></p> <hr /> <p>Marching squares</p> <p><a href="https://crieit.now.sh/upload_images/f60b1388598298537cd0325ab363572565703dce89530.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f60b1388598298537cd0325ab363572565703dce89530.gif?mw=700" alt="image" /></a></p> <hr /> <p>連立方程式を解くやつ(ガウス・ジョルダンの消去法)</p> <p><a href="https://crieit.now.sh/upload_images/921466057ccb5721b86dedc21f0caac465703de3d557d.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/921466057ccb5721b86dedc21f0caac465703de3d557d.gif?mw=700" alt="image" /></a></p> <hr /> <p>フーリエ変換</p> <p><a href="https://crieit.now.sh/upload_images/49e4aff901f6635199db6b3907c99dfd65703df2e2cb3.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/49e4aff901f6635199db6b3907c99dfd65703df2e2cb3.gif?mw=700" alt="image" /></a></p> <h1 id="簡単なCPUシミュレータ"><a href="#%E7%B0%A1%E5%8D%98%E3%81%AACPU%E3%82%B7%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF">簡単なCPUシミュレータ</a></h1> <p>今回作ったものとは無関係ですが DXOpal 関連ということでご紹介です。</p> <p>DXOpal は「ゲームを作るためのフレームワーク」という位置づけにはなっていますが、いわゆる普通のゲームを作る以外にも</p> <ul> <li>Ruby でちょっとしたお絵描きやデータ可視化をしたい(特に動きのあるもの)</li> <li>ついでにマウスやキーボードからの入力も扱ってインタラクティブにしたい</li> <li>どうせなら音も出したい</li> <li>作ったものを手軽に他の人に見せたい、使ってもらいたい</li> </ul> <p>という場合にサッと使えて重宝しています。</p> <p>そのような「ゲームではないもの」の例として、以前簡単な CPU シミュレータを作ったことがありました。</p> <ul> <li>回路を描画して、</li> <li>マウスクリックでスイッチを切り替えられて、</li> <li>リレーが切り替わる時にカシャッと音が鳴る</li> </ul> <p>というものです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/a5d6d3539e0fb8040f74">Ruby+DXOpalでリレー式論理回路シミュレータを自作して1bit CPUまで動かした</a></p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/5a779825-a252-aad1-2aab-7d51fd708363.gif" alt="x00_1bit_cpu.gif" /></p> <p>これ作るの結構楽しかったのでみなさんもぜひ! と言いたい気持ち。</p> <hr /> <p>以下の書籍にもゲーム以外のアプリケーションの例が載っています。参考までに。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nextpublishing.jp/book/11812.html">実践Opal</a>(<a target="_blank" rel="nofollow noopener" href="https://tatsu-zine.com/books/practical-opal">達人出版会</a>) <ul> <li>DXOpal を使ってお絵かきアプリを作る</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://nextpublishing.jp/book/9555.html">Pragmatic Opal Rubyで作るブラウザアプリケーション開発ガイド</a>(<a target="_blank" rel="nofollow noopener" href="https://tatsu-zine.com/books/pragmatic-opal">達人出版会</a>) <ul> <li>ICFPCビジュアライザ</li> </ul></li> </ul> <h1 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></h1> <p>せっかくのアドベントカレンダーですので、今年書いたものを貼っておきます。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/372da20581c16cbd4eea">Lramaで簡単な自作言語のパーサを書いた</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/52d0e35dc5e6e112c02f">Ruby + PyCall でデータサイエンス100本ノック(構造化データ加工編)をやってみた</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/7c81beca2c86fdf21556">PyCall.rb + Pandas: DataFrame#query の代わりにS式っぽく書けないか試してみた</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/a7da299b84f17459967c">Ruby/StringScanner: scan_full, search_full 系メソッドの整理</a></li> </ul> <h1 id="去年の"><a href="#%E5%8E%BB%E5%B9%B4%E3%81%AE">去年の</a></h1> <p>去年のアドベントカレンダーではこういうのを書いていました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/84e4c16e5602b9771e92">『RubyでつくるRuby』のMinRubyのパーサを書いた(手書きの再帰下降パーサ)</a></li> </ul> <p>「世は大パーサー時代!」より数カ月早く書いてしまってタイミングを逃した(?)感がなくもないですが、パーサにご興味あればこちらもどうぞ!</p> sonota486 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/18443 2023-06-03T18:02:29+09:00 2023-06-03T18:02:29+09:00 https://crieit.net/posts/ruby-pycall-datscience-100knocks Ruby + PyCall でデータサイエンス100本ノック(構造化データ加工編)をやってみた <p>まだ誰もやってなさそうだったのでやってみました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/100knocks-preprocess/tree/ruby-pycall">github.com/sonota88/100knocks-preprocess/tree/ruby-pycall</a></p> <p>オリジナルのリポジトリ <a target="_blank" rel="nofollow noopener" href="https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess">The-Japan-DataScientist-Society/100knocks-preprocess</a> をフォークして <code>ruby-pycall</code> ブランチを追加しています。</p> <p>GitHub でノートブックを閲覧したい場合はこちら:<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/100knocks-preprocess/blob/ruby-pycall/docker/work/answer/ans_preprocess_knock_Ruby_PyCall.ipynb">https://github.com/sonota88/100knocks-preprocess/blob/ruby-pycall/docker/work/answer/ans_preprocess_knock_Ruby_PyCall.ipynb</a></p> <hr /> <p>解答例が複数ある場合など一部TODOにしている箇所がありますが、100問全部動かすことができました(私は Python版からコピペして書き換えただけなので、これは PyCall.rb がすごい! という話です)。</p> <p>一応筆者について書いておくと、データサイエンスに関しては入門者レベルです。データフレームという概念は一応知ってるとか、Jupyter Notebook を触ったことはあるが日常的に使っているわけではない、という感じの人です。Python, Pandas, NumPy のいずれにも詳しくありません。</p> <p>:::note</p> <p>ちなみに Ruby + RedAmber 版は heronshoes さんが作成中とのことです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/heronshoes/items/962e312146dac8673b49">RedAmberで書いたらこうなる - pandasのquery - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://speakerdeck.com/heronshoes/the-adventure-of-redamber-a-data-frame-library-in-ruby?slide=43">The Adventure of RedAmber - A data frame library in Ruby - Speaker Deck</a> <ul> <li>RubyKaigi 2023</li> </ul></li> </ul> <p>:::</p> <h1 id="動かし方"><a href="#%E5%8B%95%E3%81%8B%E3%81%97%E6%96%B9">動かし方</a></h1> <p>リポジトリとブランチが違う以外はオリジナルの<a target="_blank" rel="nofollow noopener" href="https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess">データサイエンス100本ノック(構造化データ加工編)</a>の README で書かれている手順と同じです。</p> <pre><code class="sh">git clone --branch ruby-pycall https://github.com/sonota88/100knocks-preprocess.git cd 100knocks-preprocess docker compose up -d --build --wait </code></pre> <p><code>docker compose up</code> が完了するのを待ってからブラウザで <a target="_blank" rel="nofollow noopener" href="http://127.0.0.1:8888/">http://127.0.0.1:8888/</a> を開きます。</p> <p>左のメニューから <code>/work/answer/ans_preprocess_knock_Ruby_PyCall.ipynb</code> を選択してノートブックを開きます。</p> <p><a href="https://crieit.now.sh/upload_images/36b6f940e2f434c979b5ada813c8206f647b00eb4aee3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/36b6f940e2f434c979b5ada813c8206f647b00eb4aee3.png?mw=700" alt="image" /></a></p> <p>左側に Table of Contents を表示させておくと各問題に移動したいときに便利です。</p> <p><a href="https://crieit.now.sh/upload_images/7a727bbde73b2e30d4a57505e10347d2647b00fda1de4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7a727bbde73b2e30d4a57505e10347d2647b00fda1de4.png?mw=700" alt="image" /></a></p> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <h2 id="Ruby 向けのカスタマイズ"><a href="#Ruby+%E5%90%91%E3%81%91%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA">Ruby 向けのカスタマイズ</a></h2> <ul> <li>Python 版の Dockerfile をコピー+修正して Ruby 用の Dockerfile-ruby を用意する</li> <li>docker-compose.yml を修正して Dockerfile-ruby に切り替える</li> </ul> <p>やったのはこれだけですね。ここはほとんど手間がかかっていません。</p> <p>RubyData プロジェクトでメンテナンスされている <code>rubydata/datascience-notebook</code> という Docker イメージがありまして、</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/RubyData/docker-stacks">github.com/RubyData/docker-stacks</a></p> <p>これをベースイメージとして使うのがポイントです。自前でカスタマイズする必要がほとんどなく、たいへん助かりました。このイメージは便利なのでどんどん使わせてもらいましょう!(← この記事で一番強調したいのはここ)</p> <p>Python版の Dockerfile との差分はこんな感じ。</p> <pre><code class="diff">--- Dockerfile 2023-05-06 12:13:18.226946937 +0900 +++ Dockerfile-ruby 2023-05-06 12:13:18.242946463 +0900 @@ -1,5 +1,6 @@ -FROM jupyter/datascience-notebook:python-3.10.8 -#FROM jupyter/datascience-notebook:d53a302fbcd0 +# 2023-02-03 +FROM rubydata/datascience-notebook:24a7f04dfc46 + USER root ENV DEBCONF_NOWARNINGS yes @@ -29,4 +30,6 @@ && Rscript -e 'pak::repo_add(CRAN = "RSPM@2022-12-16"); pak::pak(c("DBI", "RPostgreSQL", "themis"))' \ && rm -rf Pipfile* /tmp/* /var/tmp/* +RUN pip install tabulate + HEALTHCHECK --interval=5s --retries=20 CMD ["curl", "-s", "-S", "-o", "/dev/null", "http://localhost:8888"] </code></pre> <p>ほぼベースイメージの差し替えだけで済んでいます。</p> <h2 id="DataFrame#query"><a href="#DataFrame%23query">DataFrame#query</a></h2> <p>Python版の解答例では DataFrame#query を使っている箇所がいくつかあるのですが、pandas.rb ではエラーになります(2023-05-13 時点)。そこで、次のように DataFrame#[] を使う方法で愚直に書き換えました。</p> <pre><code class="ruby"># P-004 Python版の解答例1 # df_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']] \ # .query('customer_id == &quot;CS018205000001&quot; & amount &gt;= 1000') df_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']] \ [(df_receipt['customer_id'] == "CS018205000001") & (df_receipt['amount'] >= 1000)] </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/7c81beca2c86fdf21556">PyCall.rb + Pandas: DataFrame#query の代わりにS式っぽく書けないか試してみた</a> という別記事で書いたように他の対策もありますが、今回は素朴に DataFrame#[] を使うだけにしました。なるべく素のまま書く方針です。</p> <h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/mrkn/pycall.rb">mrkn/pycall.rb: Calling Python functions from the Ruby language</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://codezine.jp/article/detail/10251">Ruby-Pythonブリッジライブラリ「PyCall」を使ってRubyでデータ分析をしよう! (1/3)|CodeZine(コードジン)</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/RubyData/workshop-materials/blob/master/iruby_guides/users_guide.md">workshop-materials/users_guide.md at master · RubyData/workshop-materials · GitHub</a> <ul> <li>IRuby Notebook 入門用テキスト</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/stat/items/a383451e7f824e4c9627">PyCall Ruby版 Tips - Qiita</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%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><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/aeacaf0ec7957715cdff">Ruby+PyCall.rbでLibreOffice Calcのオートメーションをやってみた(Ubuntu 18.04)</a></p></li> <li><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b586e96f7805a5695a34">Galaaz を触ってみた(TruffleRuby + ggplot2 で散布図を描いてみた)</a></p></li> </ul> sonota486 tag:crieit.net,2005:PublicArticle/18391 2023-02-23T15:03:54+09:00 2023-02-23T15:03:54+09:00 https://crieit.net/posts/ruby-numo-gnuplot-gem-scatterplot Ruby + Numo::Gnuplot(numo-gnuplot gem)で散布図を描く <p>@obelisk68 さんに教えてもらったサンプル(<a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/50609275668fcd7511e4#comment-0890d29e9583e74eba70">ruby_gnuplot の記事のコメント</a>, <a target="_blank" rel="nofollow noopener" href="https://obelisk.hatenablog.com/entry/2022/05/29/181912">ブログ</a>)で十分という気もしたのですが、一応自分でも触ってみました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://rubygems.org/gems/numo-gnuplot">https://rubygems.org/gems/numo-gnuplot</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/ruby-numo/numo-gnuplot">https://github.com/ruby-numo/numo-gnuplot</a></p> <h1 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h1> <ul> <li>gnuplot コマンドをインストール <ul> <li>Ubuntu 18.04 の場合は <code>sudo apt install gnuplot</code></li> </ul></li> <li>Numo::Gnuplot をインストール <ul> <li><code>gem install numo-gnuplot</code></li> </ul></li> </ul> <p>これだけ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/ruby-numo/numo-narray">Numo::NArray</a> は必須ではありません(インストールも不要)。以下は Numo::NArray なしで試しています。</p> <h1 id="最低限の例"><a href="#%E6%9C%80%E4%BD%8E%E9%99%90%E3%81%AE%E4%BE%8B">最低限の例</a></h1> <p>PNG ファイルに出力する最低限の例です。</p> <pre><code class="ruby"># sample.rb require "numo/gnuplot" xs = [1, 2, 3, 4, 5] ys = [11, 22, 33, 44, 55] Numo.gnuplot do set terminal: "png" set output: "output.png" plot xs, ys end </code></pre> <pre><code class="sh"> # 実行 ... output.png が生成される ruby sample.rb </code></pre> <p><a href="https://crieit.now.sh/upload_images/1d02f3831d34666675afe13cd9b51f8763f7011c631ae.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1d02f3831d34666675afe13cd9b51f8763f7011c631ae.png?mw=700" alt="image" /></a></p> <p>簡単ですね。<br /> ちょっと見えにくいですが、 両端の点は枠線に重なっています。</p> <h1 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h1> <pre><code class="sh">Ubuntu Linux 18.04 gnuplot -V #=> gnuplot 5.2 patchlevel 2 ruby -v #=> ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux] gem: numo-gnuplot 0.2.4 </code></pre> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/rdp/ruby_gnuplot">ruby_gnuplot</a> と同様、インストールが楽なのが良い。gnuplot コマンドのインストールさえ済んでしまえば、他にインストール絡みでトラブルが起きる余地がなさそう。気分的に楽。</li> <li><code>numo/gnuplot.rb</code> ( https://github.com/ruby-numo/numo-gnuplot/blob/master/lib/numo/gnuplot.rb ) はコメントを除くと1,000行ちょいなので、サッと読める。ruby_gnuplot よりは大きめですが。 <ul> <li><code>Numo::Gnuplot#initialize</code> で <code>IO.popen</code> したプロセスにコマンドを送る方式</li> </ul></li> </ul> <hr /> <p>ruby_gnuplot との機能比較でいえば、以下のあたりが違うようです:</p> <ul> <li>Numo::NArray に対応している</li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/SciRuby/iruby">IRuby</a> に対応している</li> <li>irb で対話的に操作できる <ul> <li>gnuplot の対話型インターフェイスの代わりに使えるっぽい</li> </ul></li> </ul> <h1 id="自分が使いそうな機能についてメモ"><a href="#%E8%87%AA%E5%88%86%E3%81%8C%E4%BD%BF%E3%81%84%E3%81%9D%E3%81%86%E3%81%AA%E6%A9%9F%E8%83%BD%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%83%A1%E3%83%A2">自分が使いそうな機能についてメモ</a></h1> <ul> <li>グラフのタイトル指定</li> <li>複数の系列の描画</li> <li>数式のプロット</li> </ul> <p>などは <a target="_blank" rel="nofollow noopener" href="https://github.com/ruby-numo/numo-gnuplot">README</a> や <a target="_blank" rel="nofollow noopener" href="https://github.com/ruby-numo/numo-gnuplot/wiki/Introduction.ja">Introduction.ja · ruby-numo/numo-gnuplot Wiki</a> にサンプルがあるので、そっちを見てください。</p> <h2 id="軸タイトル"><a href="#%E8%BB%B8%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB">軸タイトル</a></h2> <pre><code class="ruby">Numo.gnuplot do set xlabel: "x軸のラベル" set ylabel: "y軸のラベル" # ... </code></pre> <h2 id="x軸, y軸の範囲指定"><a href="#x%E8%BB%B8%2C+y%E8%BB%B8%E3%81%AE%E7%AF%84%E5%9B%B2%E6%8C%87%E5%AE%9A">x軸, y軸の範囲指定</a></h2> <pre><code class="ruby">Numo.gnuplot do set xrange: 0..6 set yrange: 0..60 # ... </code></pre> <h2 id="凡例の系列名"><a href="#%E5%87%A1%E4%BE%8B%E3%81%AE%E7%B3%BB%E5%88%97%E5%90%8D">凡例の系列名</a></h2> <pre><code class="ruby">Numo.gnuplot do plot xs, ys, title: "series name" # ... </code></pre> <p><a href="https://crieit.now.sh/upload_images/b73ba4fbec5510c877d85d0cbf8d9c0063f7013aa98af.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b73ba4fbec5510c877d85d0cbf8d9c0063f7013aa98af.png?mw=700" alt="image" /></a></p> <h2 id="日本語フォント"><a href="#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88">日本語フォント</a></h2> <p>無指定だと日本語が豆腐になりました。</p> <pre><code class="ruby">Numo.gnuplot do set terminal: "png font 'VL Gothic,20'" # ... </code></pre> <p><a href="https://crieit.now.sh/upload_images/6426107e494ed6aca64b9388221cdf2d63f7014778647.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6426107e494ed6aca64b9388221cdf2d63f7014778647.png?mw=700" alt="image" /></a></p> <h2 id="点の代わりにラベルを表示する"><a href="#%E7%82%B9%E3%81%AE%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%AB%E3%83%A9%E3%83%99%E3%83%AB%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">点の代わりにラベルを表示する</a></h2> <pre><code class="ruby">xs = [1, 2, 3, 4, 5] ys = [11, 22, 33, 44, 55] labels = ["A", "B", "C", "D", "E"] Numo.gnuplot do # ... plot xs, ys, labels, with: "labels" end </code></pre> <p><a href="https://crieit.now.sh/upload_images/06e4631861c41cafa09b2c4a22728f3f63f701524af8b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/06e4631861c41cafa09b2c4a22728f3f63f701524af8b.png?mw=700" alt="image" /></a></p> <h2 id="デバッグ出力"><a href="#%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E5%87%BA%E5%8A%9B">デバッグ出力</a></h2> <p><code>debug_on</code> を使うと、 gnuplot に送信しているコマンドを標準出力に出力することができます。</p> <pre><code class="ruby">xs = [1, 2, 3, 4, 5] ys = [11, 22, 33, 44, 55] Numo.gnuplot do debug_on set terminal: "png" set output: "output.png" plot xs, ys end </code></pre> <pre><code class="sh">$ ruby sample_debug.rb <set terminal png <set output "output.png" <plot '-' <1 11 <2 22 <3 33 <4 44 <5 55 <e </code></pre> <p>行頭の <code><</code> を取り除いてやれば、そのまま gnuplot に渡せるようです。</p> <pre><code class="sh">$ ruby sample_debug.rb | sed -e 's/^<//g' > sample.gs $ cat sample.gs set terminal png set output "output.png" plot '-' 1 11 2 22 3 33 4 44 5 55 e # スクリプトを gnuplot コマンドに渡して実行 $ gnuplot sample.gs </code></pre> <h1 id="まとめたもの"><a href="#%E3%81%BE%E3%81%A8%E3%82%81%E3%81%9F%E3%82%82%E3%81%AE">まとめたもの</a></h1> <p>せっかくなのでいろいろ含めたものも貼ってみます。<br /> @obelisk68 さんが書いてくださったのとほぼ同じ。</p> <pre><code class="ruby">require "numo/gnuplot" xs = [1.0, 1.8, 3.1] ys1 = [11, 25, 32] ys2 = [21, 35, 42] ys3 = [31, 45, 52] Numo.gnuplot do set terminal: "png font 'Noto Sans CJK JP,12'" set output: "output.png" set title: "グラフのタイトル" set xrange: 0..6 set yrange: 0..60 set xlabel: "x軸のラベル" set ylabel: "y軸のラベル" plot xs, ys1, { title: "系列1(点のみ)" }, xs, ys2, { title: "系列2(線のみ)", with: :lines }, xs, ys3, { title: "系列3(点と線)", with: :linespoints }, "x * 10", { title: "y = x * 10" } end </code></pre> <p><a href="https://crieit.now.sh/upload_images/0a564211e53eb0221887ee8da4ecd21163f7016136f2e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0a564211e53eb0221887ee8da4ecd21163f7016136f2e.png?mw=700" alt="image" /></a></p> <p><code>with: :lines</code> のように指定するか、または数式を使えば任意の直線が描画できるので、回帰直線もこれでいけますね(フィッティングの話については割愛)。</p> <h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/ruby-numo/numo-gnuplot-demo">ruby-numo/numo-gnuplot-demo: Ruby/Numo::Gnuplot Demo</a></p> <p>めちゃくちゃいっぱいある gnuplot 本家のサンプルを Numo::Gnuplot に書き直した版。README から辿れる情報ですが一応。</p> <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> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/50609275668fcd7511e4">Ruby + ruby_gnuplot(gnuplot gem)で散布図を描く</a></p> <p>ruby_gnuplot もシンプルで良いと思いますよ。</p> <hr /> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/86afece3b2595648a656">SVG::Graph(svg-graph gem)で散布図を描く</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b586e96f7805a5695a34">Galaaz を触ってみた(TruffleRuby + ggplot2 で散布図を描いてみた)</a></p> 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/18359 2022-12-29T18:12:03+09:00 2022-12-29T18:20:09+09:00 https://crieit.net/posts/ruby-gnuplot-gem-scatterplot Ruby + ruby_gnuplot(gnuplot gem)で散布図を描く <p><a target="_blank" rel="nofollow noopener" href="https://rubygems.org/gems/gnuplot">https://rubygems.org/gems/gnuplot</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/rdp/ruby_gnuplot">https://github.com/rdp/ruby_gnuplot</a></p> <h1 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h1> <ul> <li>gnuplot コマンドをインストール <ul> <li>Ubuntu 18.04 の場合は <code>sudo apt install gnuplot</code></li> </ul></li> <li>ruby_gnuplot をインストール <ul> <li><code>gem install gnuplot</code></li> </ul></li> </ul> <p>これだけ。</p> <p>ただし、 Ruby 3.1.0 で matrix が bundled gem になったようなので、3.1.0 以降の場合は <code>gem install matrix</code> も必要です。<br /> 参考: <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/shuichi/articles/bundled-gem-with-gemfile">[Ruby] Bundled gemsはGemfileに指定して使おう</a></p> <h1 id="最低限の例"><a href="#%E6%9C%80%E4%BD%8E%E9%99%90%E3%81%AE%E4%BE%8B">最低限の例</a></h1> <p>PNG ファイルに出力する最低限の例です。</p> <pre><code class="ruby"># sample.rb require "gnuplot" Gnuplot.open do |gp| Gnuplot::Plot.new(gp) do |plot| plot.terminal "png" plot.output "output.png" x = [1, 2, 3, 4, 5] y = [11, 22, 33, 44, 55] plot.data << Gnuplot::DataSet.new([x, y]) end end </code></pre> <pre><code class="sh"> # 実行 ... output.png が生成される ruby sample.rb </code></pre> <p><a href="https://crieit.now.sh/upload_images/36b6f940e2f434c979b5ada813c8206f63ad5a4b7d18e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/36b6f940e2f434c979b5ada813c8206f63ad5a4b7d18e.png?mw=700" alt="image" /></a></p> <p>以上。簡単ですね。<br /> ちょっと見えにくいですが、 両端の点は枠線に重なっています。</p> <p>PNG 以外にも、 SVG や PDF として出力することもできます。</p> <h1 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h1> <pre><code class="sh">Ubuntu Linux 18.04 gnuplot -V #=> gnuplot 5.2 patchlevel 2 ruby -v #=> ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux] gem: gnuplot 2.6.2 matrix 0.4.2 </code></pre> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <ul> <li>とにかくインストールが楽なのが良い。gnuplot コマンドのインストールさえ済んでしまえば、他にインストール絡みでトラブルが起きる余地がなさそう。気分的に楽 👌</li> <li>gnuplot でできることは大体何でもできるはず。たぶん。</li> <li><code>gnuplot.rb</code> ( https://github.com/rdp/ruby_gnuplot/blob/master/lib/gnuplot.rb ) はコメントを除くと300行くらいなので、サッと読める <ul> <li>gnuplot 用のスクリプトに変換して gnuplot コマンドに渡しているだけ</li> </ul></li> <li>「○○がやりたいけどどうすればいい?」となった場合は gnuplot 向けの情報を探す <ul> <li>gnuplot は枯れたプロダクトなので、情報はたくさんある</li> <li>すでに gnuplot に習熟している人は楽そう</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/red-data-tools/charty">Charty</a> のバックエンドとして使える?(まだ試していません) <ul> <li>(追記) 2022-05-29 現在では未対応でした</li> </ul></li> </ul> <p>非常に薄いラッパーなので、素の gnuplot をそのまま使うのと実はそんなに大差なくて、そこを踏まえた上で使う分には便利。<br /> 「gnuplot の使い方を覚えたいわけではないんだけどなあ……」という人には向かないかもしれません。</p> <h1 id="自分が使いそうな機能についてメモ"><a href="#%E8%87%AA%E5%88%86%E3%81%8C%E4%BD%BF%E3%81%84%E3%81%9D%E3%81%86%E3%81%AA%E6%A9%9F%E8%83%BD%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%83%A1%E3%83%A2">自分が使いそうな機能についてメモ</a></h1> <ul> <li>グラフのタイトル指定</li> <li>x軸, y軸の範囲指定、軸タイトル指定</li> <li>複数の系列の描画</li> <li>数式のプロット</li> </ul> <p>などは <a target="_blank" rel="nofollow noopener" href="https://github.com/rdp/ruby_gnuplot">README</a> にサンプルがあるので、そっちを見てください。</p> <h2 id="系列名の凡例"><a href="#%E7%B3%BB%E5%88%97%E5%90%8D%E3%81%AE%E5%87%A1%E4%BE%8B">系列名の凡例</a></h2> <p><code>DataSet.new</code> にブロックを付けると、ブロックパラメータとして DataSet のインスタンスが渡されるので、それを使って指定できる。</p> <pre><code class="ruby">plot.data << Gnuplot::DataSet.new([x, y]) { |ds| ds.title = "dataset" } </code></pre> <p><a href="https://crieit.now.sh/upload_images/7a727bbde73b2e30d4a57505e10347d263ad5aa0bf78c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7a727bbde73b2e30d4a57505e10347d263ad5aa0bf78c.png?mw=700" alt="image" /></a></p> <h2 id="日本語フォント"><a href="#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88">日本語フォント</a></h2> <p>無指定だと日本語が豆腐になりました。<br /> <code>gnuplot フォント 日本語 set terminal</code> のような検索ワードで調べると、 <code>Plot#terminal</code> で指定できることが分かります。</p> <pre><code class="ruby">Gnuplot.open do |gp| Gnuplot::Plot.new(gp) do |plot| plot.terminal "png font 'VL Gothic,20'" plot.output "output.png" x = [1, 2, 3] y1 = [11, 25, 32] plot.data << Gnuplot::DataSet.new([x, y1]) { |ds| ds.title = "系列1" } end end </code></pre> <p><a href="https://crieit.now.sh/upload_images/34a1b30f9276e1831c27ee024469926b63ad5aaf0c674.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/34a1b30f9276e1831c27ee024469926b63ad5aaf0c674.png?mw=700" alt="image" /></a></p> <h2 id="点の代わりにラベルを表示する"><a href="#%E7%82%B9%E3%81%AE%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%AB%E3%83%A9%E3%83%99%E3%83%AB%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">点の代わりにラベルを表示する</a></h2> <pre><code class="ruby">Gnuplot.open do |gp| Gnuplot::Plot.new(gp) do |plot| plot.terminal "png" plot.output "output.png" x = [1, 2, 3] y = [11, 22, 33] label = ["A", "B", "C"] plot.data << Gnuplot::DataSet.new([x, y, label]) { |ds| ds.with = "labels" } end end </code></pre> <p><a href="https://crieit.now.sh/upload_images/0c359f4284f6db1f1eeab9708335763163ad5abc9417f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0c359f4284f6db1f1eeab9708335763163ad5abc9417f.png?mw=700" alt="image" /></a></p> <hr /> <p>点とラベルを両方表示するのは若干面倒そう。</p> <p>In gnuplot, how to label each point in the plot with its coordinates? - Stack Overflow<br /> <a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/23177716/in-gnuplot-how-to-label-each-point-in-the-plot-with-its-coordinates">https://stackoverflow.com/questions/23177716/in-gnuplot-how-to-label-each-point-in-the-plot-with-its-coordinates</a></p> <h2 id="生成されたスクリプトだけ欲しい"><a href="#%E7%94%9F%E6%88%90%E3%81%95%E3%82%8C%E3%81%9F%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%A0%E3%81%91%E6%AC%B2%E3%81%97%E3%81%84">生成されたスクリプトだけ欲しい</a></h2> <p>以下のように <code>Gnuplot.open</code> にモンキーパッチを当てると、グラフの描画は行わずに gnuplot 用に生成されたスクリプトの出力だけを行うことができます。</p> <pre><code class="ruby">module Gnuplot def self.open(persist = true) io = $stdout yield io end end Gnuplot.open do |gp| Gnuplot::Plot.new(gp) do |plot| x = [1, 2, 3] y = [11, 22, 33] plot.data << Gnuplot::DataSet.new([x, y]) end end </code></pre> <pre><code class="sh">$ ruby sample_print_script.rb > sample.gs $ cat sample.gs plot '-' 1 11 2 22 3 33 e # スクリプトを gnuplot コマンドに渡して実行 $ gnuplot -persist sample.gs </code></pre> <h1 id="まとめたもの"><a href="#%E3%81%BE%E3%81%A8%E3%82%81%E3%81%9F%E3%82%82%E3%81%AE">まとめたもの</a></h1> <p>せっかくなのでいろいろ含めたものも貼ってみます。</p> <pre><code class="ruby">require "gnuplot" Gnuplot.open do |gp| Gnuplot::Plot.new(gp) do |plot| plot.terminal "png font 'Noto Sans CJK JP,12'" plot.output "output.png" plot.title "グラフのタイトル" plot.xrange "[0:6]" plot.yrange "[0:60]" plot.xlabel "x軸のラベル" plot.ylabel "y軸のラベル" x = [1, 2, 3] y1 = [11, 25, 32] y2 = [21, 35, 42] y3 = [31, 45, 52] plot.data << Gnuplot::DataSet.new([x, y1]) { |ds| ds.title = "系列1" } plot.data << Gnuplot::DataSet.new([x, y2]) { |ds| ds.title = "系列2(線のみ)" ds.with = "lines" } plot.data << Gnuplot::DataSet.new([x, y3]) { |ds| ds.title = "系列3(点と線)" ds.with = "linespoints" } # 数式 plot.data << Gnuplot::DataSet.new("x * 10") { |ds| ds.title = "y = x * 10" } end end </code></pre> <p><a href="https://crieit.now.sh/upload_images/dda408a296d12511a205c05e56b2e0e563ad58ecdcfc6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/dda408a296d12511a205c05e56b2e0e563ad58ecdcfc6.png?mw=700" alt="image" /></a></p> <p><code>ds.with = "lines"</code> を使うか、または数式を使えば任意の直線が描画できるので、回帰直線もこれでいけますね(フィッティングの話については割愛)。</p> <h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1> <p>Rubyのgnuplotで時系列データをプロット | Lifelog<br /> <a target="_blank" rel="nofollow noopener" href="https://ytkyk.info/blog/2016/07/21/rubyのgnuplotで時系列データをプロット/">https://ytkyk.info/blog/2016/07/21/rubyのgnuplotで時系列データをプロット/</a></p> <hr /> <p>gnuplot documentation<br /> <a target="_blank" rel="nofollow noopener" href="http://gnuplot.info/documentation.html">http://gnuplot.info/documentation.html</a></p> <p>日本語のPDFがあります。</p> <hr /> <p>Demos for gnuplot version 5.4<br /> <a target="_blank" rel="nofollow noopener" href="http://gnuplot.sourceforge.net/demo_5.4/">http://gnuplot.sourceforge.net/demo_5.4/</a></p> <p>豊富なサンプル。gnuplot マジ何でもできるな、という気持ちになります。</p> <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/86afece3b2595648a656">SVG::Graph(svg-graph gem)で散布図を描く</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b586e96f7805a5695a34">Galaaz を触ってみた(TruffleRuby + ggplot2 で散布図を描いてみた)</a></li> </ul> sonota486 tag:crieit.net,2005:PublicArticle/18295 2022-09-19T15:01:30+09:00 2022-09-19T15:07:22+09:00 https://crieit.net/posts/ubuntu-2204-jupyter-notebook-iruby-pyenv-rbenv-bundler Ubuntu 22.04にJupyter NotebookとIRubyをインストール(pyenv, rbenv, Bundler を使用) <ul> <li><del>それぞれのバージョンが上がった以外は<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/06/07/112841">前回の Ubuntu 18.04 のとき</a>からあまり変わっていません</del> <ul> <li>Bundler の binstubs を利用する手順を追加しました</li> </ul></li> <li>手軽に試したい場合は公式の Docker イメージや Binder を利用するのが良いと思います(<a target="_blank" rel="nofollow noopener" href="https://github.com/SciRuby/iruby/blob/master/README.md">IRuby の README</a> を参照)</li> </ul> <hr /> <p><strong>大事なので2回書きます。手軽に試したい場合は公式の Docker イメージを使うか、Binder を使いましょう(<a target="_blank" rel="nofollow noopener" href="https://github.com/SciRuby/iruby/blob/master/README.md">IRuby の README</a> を参照)。 下記のような面倒なインストール手順は不要ですぐ使えます。</strong></p> <h1 id="バージョンなど"><a href="#%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%81%AA%E3%81%A9">バージョンなど</a></h1> <pre><code>Ubuntu 18.04(コンテナ外の作業環境) anyenv pyenv Python 3.10.3 rbenv Ruby 3.1.2 jupyter 1.0.0 jupyter_core 4.10.0 notebook 6.4.11 iruby 0.7.4 </code></pre> <h1 id="Docker の用意"><a href="#Docker+%E3%81%AE%E7%94%A8%E6%84%8F">Docker の用意</a></h1> <p>Docker イメージを作るのが目的ではなく、まっさらな状態に戻してあれこれ試すために使います。<br /> Docker なしでも大体同じだと思います。</p> <pre><code class="sh"># Dockerfile FROM ubuntu:22.04 RUN apt-get update \ && apt-get install -y sudo git wget build-essential nano RUN useradd --create-home --gid sudo --shell /bin/bash user1 \ && echo 'user1:pass' | chpasswd \ && echo 'Defaults visiblepw' >> /etc/sudoers \ && echo 'user1 ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers USER user1 WORKDIR /home/user1 CMD ["/bin/bash"] </code></pre> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://qiita.com/iganari/items/1d590e358a029a1776d6">Dockerコンテナ内にsudoユーザを追加する - Qiita</a></p> <p>ユーザ名 <code>user1</code> は適当なものです。適宜読み替えます。以下同様。</p> <p>イメージをビルドしてコンテナを起動。</p> <pre><code>docker build -t ubuntu_jupyter:22.04 . docker run --rm -it -p8888:8888 ubuntu_jupyter:22.04 bash </code></pre> <p>以下はコンテナ内で作業しています。</p> <h1 id="anyenv, rbenv, pyenv のインストール"><a href="#anyenv%2C+rbenv%2C+pyenv+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">anyenv, rbenv, pyenv のインストール</a></h1> <pre><code class="sh">git clone https://github.com/anyenv/anyenv ~/.anyenv ## 一応バージョン指定 ( cd ~/.anyenv; git checkout v1.1.4; git status ) echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bashrc echo 'eval "$(anyenv init -)"' >> ~/.bashrc exec bash -l ## メッセージにしたがって実行 yes | anyenv install --init anyenv install rbenv anyenv install pyenv ## 一応バージョン指定 ( cd ~/.anyenv/envs/rbenv; git checkout v1.2.0; git status ) ( cd ~/.anyenv/envs/pyenv; git checkout v2.2.5; git status ) exec bash -l </code></pre> <h1 id="Ruby のインストール"><a href="#Ruby+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Ruby のインストール</a></h1> <pre><code>sudo apt install -y libssl-dev zlib1g-dev rbenv install 3.1.2 </code></pre> <p>Docker のポートマッピングの確認。先に疎通確認しておきます。</p> <pre><code class="sh">rbenv shell 3.1.2 gem install webrick ruby -run -e httpd -- --port=8888 --bind-address=0.0.0.0 . </code></pre> <p>ホスト側から http://localhost:8888/ にアクセスできることを確認して Ctrl-C で止める。</p> <h1 id="Python のインストール"><a href="#Python+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Python のインストール</a></h1> <pre><code class="sh">sudo apt install -y libffi-dev libsqlite3-dev env PYTHON_CONFIGURE_OPTS='--enable-shared' pyenv install 3.10.3 </code></pre> <p><code>env PYTHON_CONFIGURE_OPTS='--enable-shared'</code> は後で PyCall を使うための指定<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/mrkn/pycall.rb#note-for-pyenv-users">https://github.com/mrkn/pycall.rb#note-for-pyenv-users</a></p> <h1 id="Jupyter Notebook のインストール"><a href="#Jupyter+Notebook+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Jupyter Notebook のインストール</a></h1> <pre><code class="sh">## ディレクトリと rbenv, pyenv の用意 mkdir ${HOME}/jupyter cd ${HOME}/jupyter pwd #=> /home/user1/jupyter rbenv local 3.1.2 pyenv local 3.10.3 ruby -v #=> ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux] python -V #=> Python 3.10.3 </code></pre> <pre><code class="sh">## bundle init みたいなもの ## &quot;venv.d&quot; は任意のディレクトリ名 python -m venv venv.d ## 常に bundle exec してるみたいなモードになる ## モードを抜けたい場合は deactivate を実行する . venv.d/bin/activate ## bundle add みたいなもの pip install jupyter jupyter notebook --no-browser --ip=0.0.0.0 ## ログイン用のトークン付きのURLが表示されるので、ホスト側のブラウザで開く </code></pre> <p><code>--ip=0.0.0.0</code> を指定しているのは Docker コンテナ内で実行してホスト側から参照するため。</p> <p>確認できたら Ctrl-C で止める。</p> <h1 id="IRuby のインストール"><a href="#IRuby+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">IRuby のインストール</a></h1> <p>基本的には<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/SciRuby/iruby">https://github.com/SciRuby/iruby</a><br /> の説明を参考にしてインストールしました。</p> <p>ただし、gem を Bundler で管理したいので、そのための手順を加えています。</p> <p>Bundler 環境で iruby を実行するための方法として</p> <ul> <li>binstubs を使う方法</li> <li>ラッパースクリプトを使う方法</li> </ul> <p>の2つを試しました。 binstubs を使う方が手順が簡単です。</p> <h2 id="binstubs を使う場合"><a href="#binstubs+%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88">binstubs を使う場合</a></h2> <pre><code class="sh">sudo apt install -y libtool libffi-dev ruby ruby-dev make sudo apt install -y libzmq3-dev libczmq-dev bundle init bundle config set --local path 'vendor/bundle' bundle add ffi-rzmq iruby pycall rake bundle binstubs iruby ## ./bin/iruby が生成される bin/iruby register --force ## ~/.local/share/jupyter/kernels/ruby/kernel.json ## が生成される cat ~/.local/share/jupyter/kernels/ruby/kernel.json #=> {"argv":["/home/user1/jupyter/bin/iruby","kernel","{connection_file}"],"display_name":"Ruby 3.1.2","language":"ruby"} ## この状態で jupyter 起動 jupyter notebook --no-browser --ip=0.0.0.0 ## → Ruby 3.1.2 のノートブックが新規作成できるようになる ## (メニューに表示されるようになる) </code></pre> <p><a href="https://crieit.now.sh/upload_images/a28566d432610fa1952ea6c4f741348c63280709d5558.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a28566d432610fa1952ea6c4f741348c63280709d5558.png?mw=700" alt="image" /></a></p> <h2 id="ラッパースクリプトを使う場合"><a href="#%E3%83%A9%E3%83%83%E3%83%91%E3%83%BC%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88">ラッパースクリプトを使う場合</a></h2> <pre><code class="sh">sudo apt install -y libtool libffi-dev ruby ruby-dev make sudo apt install -y libzmq3-dev libczmq-dev bundle init bundle config set --local path 'vendor/bundle' bundle add ffi-rzmq iruby pycall rake bundle exec iruby register --force ## ~/.local/share/jupyter/kernels/ruby/kernel.json ## が生成される cat ~/.local/share/jupyter/kernels/ruby/kernel.json #=> {"argv":["/home/user1/jupyter/vendor/bundle/ruby/3.1.0/bin/iruby","kernel","{connection_file}"],"display_name":"Ruby 3.1.2","language":"ruby"} ## この状態で jupyter 起動 jupyter notebook --no-browser --ip=0.0.0.0 ## → Ruby 3.1.2 のノートブックが新規作成できるようになる ## (メニューに表示されるようになる) </code></pre> <p><a href="https://crieit.now.sh/upload_images/a28566d432610fa1952ea6c4f741348c63280709d5558.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a28566d432610fa1952ea6c4f741348c63280709d5558.png?mw=700" alt="image" /></a></p> <p>しかし、実際に新規作成を行うと次のようなエラーが jupyter を起動したターミナルに出てうまく動かない。</p> <pre><code>[I 05:17:21.886 NotebookApp] KernelRestarter: restarting kernel (3/5), new random ports /home/user1/.anyenv/envs/rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems.rb:265:in `find_spec_for_exe': can't find gem iruby (>= 0.a) with executable iruby (Gem::GemNotFoundException) from /home/user1/.anyenv/envs/rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems.rb:284:in `activate_bin_path' from /home/user1/jupyter/vendor/bundle/ruby/3.1.0/bin/iruby:25:in `<main>' </code></pre> <p>これはおそらく rbenv + bundler 環境で実行できていないせいなので、iruby コマンドのラッパー <code>iruby.sh</code> を用意して対処してみる。<br /> (他に良い方法があるかもしれませんが、とりあえずこれで動きました。)</p> <pre><code class="sh">cat <<'EOB' > iruby.sh #!/bin/bash JUPYTER_DIR=~/jupyter export PYENV_ROOT="${HOME}/.anyenv/envs/pyenv" export LIBPYTHON=${PYENV_ROOT}/versions/3.10.3/lib/libpython3.10.so.1.0 export PYTHON=${JUPYTER_DIR}/venv.d/bin/python # これでもいい? # export PYTHON=${PYENV_ROOT}/shims/python export RBENV_ROOT="${HOME}/.anyenv/envs/rbenv" export PATH="${RBENV_ROOT}/bin:${PATH}" eval "$(rbenv init -)" rbenv shell 3.1.2 BUNDLE_GEMFILE=${JUPYTER_DIR}/Gemfile \ bundle exec iruby "$@" EOB </code></pre> <pre><code class="sh">## 実行権限を付ける chmod u+x iruby.sh ## iruby のパスを修正 nano ~/.local/share/jupyter/kernels/ruby/kernel.json { "argv": [ "/home/user1/jupyter/iruby.sh", ...ここだけ修正 "kernel", "{connection_file}" ], "display_name":"Ruby 3.1.2", "language":"ruby" } ## もう一度 jupyter を起動 jupyter notebook --no-browser --ip=0.0.0.0 </code></pre> <p>これで ruby カーネルが動くようになった。</p> <h1 id="IRuby のノートブックを使う"><a href="#IRuby+%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%88%E3%83%96%E3%83%83%E3%82%AF%E3%82%92%E4%BD%BF%E3%81%86">IRuby のノートブックを使う</a></h1> <p>基本的な使い方については <a target="_blank" rel="nofollow noopener" href="https://github.com/RubyData/workshop-materials/blob/master/iruby_guides/users_guide.md">IRuby Notebook 利用者ガイド</a> が参考になります。</p> <p>matplotlib を使っている箇所があるので、下記でインストールしておく必要があります。</p> <pre><code>pip install matplotlib bundle add matplotlib </code></pre> <h1 id="ライブラリの追加"><a href="#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E8%BF%BD%E5%8A%A0">ライブラリの追加</a></h1> <h2 id="Python のライブラリを追加したい場合"><a href="#Python+%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88">Python のライブラリを追加したい場合</a></h2> <pre><code class="sh">pip install lib_foo </code></pre> <h2 id="Ruby のライブラリを追加したい場合"><a href="#Ruby+%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88">Ruby のライブラリを追加したい場合</a></h2> <pre><code class="sh">bundle add lib_foo </code></pre> <p>カーネルを再起動(ノートブックのページのメニューの Kernal → Restart)すると require できるようになる。</p> <h1 id="バージョン詳細"><a href="#%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E8%A9%B3%E7%B4%B0">バージョン詳細</a></h1> <p>pip freeze</p> <pre><code>argon2-cffi==21.3.0 argon2-cffi-bindings==21.2.0 asttokens==2.0.5 attrs==21.4.0 backcall==0.2.0 beautifulsoup4==4.11.1 bleach==5.0.0 cffi==1.15.0 debugpy==1.6.0 decorator==5.1.1 defusedxml==0.7.1 entrypoints==0.4 executing==0.8.3 fastjsonschema==2.15.3 ipykernel==6.13.0 ipython==8.3.0 ipython-genutils==0.2.0 ipywidgets==7.7.0 jedi==0.18.1 Jinja2==3.1.2 jsonschema==4.5.1 jupyter==1.0.0 jupyter-client==7.3.0 jupyter-console==6.4.3 jupyter-core==4.10.0 jupyterlab-pygments==0.2.2 jupyterlab-widgets==1.1.0 MarkupSafe==2.1.1 matplotlib-inline==0.1.3 mistune==0.8.4 nbclient==0.6.2 nbconvert==6.5.0 nbformat==5.4.0 nest-asyncio==1.5.5 notebook==6.4.11 packaging==21.3 pandocfilters==1.5.0 parso==0.8.3 pexpect==4.8.0 pickleshare==0.7.5 prometheus-client==0.14.1 prompt-toolkit==3.0.29 psutil==5.9.0 ptyprocess==0.7.0 pure-eval==0.2.2 pycparser==2.21 Pygments==2.12.0 pyparsing==3.0.8 pyrsistent==0.18.1 python-dateutil==2.8.2 pyzmq==22.3.0 qtconsole==5.3.0 QtPy==2.1.0 Send2Trash==1.8.0 six==1.16.0 soupsieve==2.3.2.post1 stack-data==0.2.0 terminado==0.13.3 tinycss2==1.1.1 tornado==6.1 traitlets==5.1.1 wcwidth==0.2.5 webencodings==0.5.1 widgetsnbextension==3.6.0 </code></pre> <p>jupyter --version</p> <pre><code>Selected Jupyter core packages... IPython : 8.3.0 ipykernel : 6.13.0 ipywidgets : 7.7.0 jupyter_client : 7.3.0 jupyter_core : 4.10.0 jupyter_server : not installed jupyterlab : not installed nbclient : 0.6.2 nbconvert : 6.5.0 nbformat : 5.4.0 notebook : 6.4.11 qtconsole : 5.3.0 traitlets : 5.1.1 </code></pre> <p>cat Gemfile.lock</p> <pre><code>GEM remote: https://rubygems.org/ specs: data_uri (0.1.0) ffi (1.15.5) ffi-rzmq (2.0.7) ffi-rzmq-core (>= 1.0.7) ffi-rzmq-core (1.0.7) ffi io-console (0.5.11) irb (1.4.1) reline (>= 0.3.0) iruby (0.7.4) data_uri (~> 0.1) ffi-rzmq irb mime-types (>= 3.3.1) multi_json (~> 1.11) native-package-installer mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) multi_json (1.15.0) native-package-installer (1.1.4) pycall (1.4.1) rake (13.0.6) reline (0.3.1) io-console (~> 0.5) PLATFORMS x86_64-linux DEPENDENCIES ffi-rzmq (~> 2.0) iruby (~> 0.7.4) pycall (~> 1.4) rake (~> 13.0) BUNDLED WITH 2.3.7 </code></pre> <h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1> <ul> <li>2018-04 <a target="_blank" rel="nofollow noopener" href="https://github.com/RubyData/workshop-materials/blob/master/iruby_guides/installation_guide.md">IRuby インストールガイド (RubyData/workshop-materials)</a></li> <li>2017-05 <a target="_blank" rel="nofollow noopener" href="https://qiita.com/methane/items/5afdabd513a18049c34f">Rubyist が pyenv を使うときに知っておいてほしいこと - Qiita</a></li> </ul> <hr /> <ul> <li>2019-12 <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kojix2/items/456259f40d5563005cd1">Rubyでグラフを描画するツール GR.rb の紹介 - Qiita</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%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><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/86afece3b2595648a656">SVG::Graph(svg-graph gem)で散布図を描く</a></p></li> <li><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/da02c6f9f85c5a33f0a1">Ruby + Numo::Gnuplot(numo-gnuplot gem)で散布図を描く</a></p></li> <li><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b586e96f7805a5695a34">Galaaz を触ってみた(TruffleRuby + ggplot2 で散布図を描いてみた)</a></p></li> </ul> sonota486 tag:crieit.net,2005:PublicArticle/18273 2022-08-07T14:30:11+09:00 2022-08-07T14:30:55+09:00 https://crieit.net/posts/opal-builder-compile-source-map Opal::Builder を使ってコンパイルする際に source map も生成する <p><a target="_blank" rel="nofollow noopener" href="https://github.com/opal/opal/blob/v1.5.0/lib/opal/cli_runners/compiler.rb">cli_runners/compiler.rb</a> を参考にしました。Opal のバージョンは v1.5.0 です。</p> <pre><code class="ruby">require "opal" builder = Opal::Builder.new builder.build("./sample.rb") compiled_source = builder.to_s compiled_source += "\n" + builder.source_map.to_data_uri_comment File.write("sample.js", compiled_source) </code></pre> <p>ちなみに、 opal コマンドを使って <code>opal -c sample.rb</code> のようにコンパイルした場合はデフォルトで source map を生成してくれます( <code>--no-source-map</code> オプションで無効化できる)。</p> <h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/crane_house/items/2d0059c7d35542509397">DxOpalのゲームをコンパクトにして公開する方法 - Qiita</a></li> </ul> sonota486 tag:crieit.net,2005:PublicArticle/18245 2022-07-17T13:24:18+09:00 2022-07-17T13:24:40+09:00 https://crieit.net/posts/mal-lisp-template-engine malでかんたんなテンプレートエンジンを書いてみた <p>これは <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2021/lisp">Lisp Advent Calendar 2021</a> の25日目の記事です。Qiita に書いていた記事のクロスポストです。</p> <hr /> <p>言語非依存なテンプレートエンジンがあったらいいなと昔からボンヤリ考えていた(切実にほしいという程ではない)のですが、<a target="_blank" rel="nofollow noopener" href="https://github.com/kanaka/mal">mal(Make a Lisp)</a>で作ったらどうだろうと思って試しにやってみました。「なんかやってみたくなってやってみた」系の記事です。</p> <p>Lisp を書くのは年に数回という感じの人が書いていますので、拙いところは大目に見ていただければと思います。</p> <p>↓ちなみに去年のアドベントカレンダーではこんなのを書きました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/501f0efa437bc4d53cc7">LibreOffice BasicでLispインタプリタ(mal)を書いた - Qiita</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/malten">https://github.com/sonota88/malten</a></p> <p><code>sonota88/mal</code> をサブモジュールとして使っているので、 <code>git clone --recursive ...</code> でクローンする必要があります。</p> <h1 id="Ruby版"><a href="#Ruby%E7%89%88">Ruby版</a></h1> <p>Ruby 版だとこんな感じ。</p> <pre><code class="ruby">template = <<TEMPLATE <h1>品目一覧</h1> <p>購入日: <%= date %></p> <table> <tr> <th>ID</th> <th>品名</th> <th>価格</th> </tr> <% (map (fn* [item] (do %> <tr> <td><%= (get item "id") %></td> <td><%= (get item "name") %></td> <td><%= (- (get item "price") (if (get item "discount") (get item "discount") 0)) %> 円</td> </tr> <% )) items) %> </table> TEMPLATE context = { date: "2021-12-25", items: [ { id: 1, name: "foo", price: 100, discount: nil }, { id: 2, name: "bar", price: 200, discount: nil }, { id: 3, name: "baz", price: 300, discount: 50 } ] } rendered = Malten.render(context, template) print rendered </code></pre> <p><code><% ... %></code> または <code><%= ... %></code> の中にコードを書きます。ERB や JSP に倣ってベタなスタイルにしましたが、コードの部分が mal なので、見たことあるような、ないような、怪しげな雰囲気ですね。</p> <hr /> <p>出力も一応貼っておきます。</p> <pre><code class="html"><br /><h1>品目一覧</h1> <p>購入日: 2021-12-25</p> <table> <tr> <th>ID</th> <th>品名</th> <th>価格</th> </tr> <tr> <td>1</td> <td>foo</td> <td>100 円</td> </tr> <tr> <td>2</td> <td>bar</td> <td>200 円</td> </tr> <tr> <td>3</td> <td>baz</td> <td>250 円</td> </tr> </table> </code></pre> <h1 id="Java版"><a href="#Java%E7%89%88">Java版</a></h1> <p>Java 版も作ってみました(テンプレートは同じなので省略)。</p> <pre><code class="java"> String template = getTemplate(); Context context = new Context(); context.map.put("date", "2021-12-25"); { List<Map<String, Object>> items = new ArrayList<>(); items.add(new Item(1, "foo", 100, null).toPlain()); items.add(new Item(2, "bar", 200, null).toPlain()); items.add(new Item(3, "baz", 300, 50).toPlain()); context.map.put("items", items); } String rendered = Malten.render(context, template); System.out.println(rendered); </code></pre> <p>大体似たような感じですね。</p> <h1 id="概観"><a href="#%E6%A6%82%E8%A6%B3">概観</a></h1> <p>大雑把な処理の流れ。</p> <pre><code>テンプレートテキスト ↓ ↓レンダリング用コード生成 ↓ レンダリング用コード(mal のコード) ↓ ↓eval(ここでコンテキストを参照) ↓ 出力テキスト </code></pre> <h1 id="レンダリング用コード生成"><a href="#%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%E7%94%9F%E6%88%90">レンダリング用コード生成</a></h1> <p>たとえば、次のようなテンプレートテキストがあったとして、</p> <pre><code class="erb"># 品目一覧 <% (map (fn* [item] (do %> - <% (print (get item "name")) %> <% (print (get item "price")) %> 円 <% )) items) %> </code></pre> <p>これを変換すると次のような mal のコードになる。元のテンプレートと比べて眺めると何やってるかなんとなく分かりますよね。</p> <pre><code class="clojure">(print "# 品目一覧\n\n") (map (fn* [item] (do (print "\n\n- ") (print (get item "name")) (print " ") (print (get item "price")) (print " 円\n\n") )) items) </code></pre> <p>下請けの関数などは除いて骨組部分だけ貼ります。</p> <pre><code class="clojure">;; malten.mal ;; <% ... %> の中身の長さを求める (def! mal-code-length (fn* [rest] (let* [iter (fn* [rest2 pos] (if (s#start-with? rest2 "%>") pos (iter (s#rest rest2) (+ pos 1)))) ] (iter rest 0)))) ;; <% ... %> の部分の処理 (def! gen-renderer-code (fn* [rest buf acc] (let* [len (mal-code-length rest)] (gen-renderer-text (s#drop rest (+ len 2)) "" ; clear buf (cons (if (s#start-with? rest "=") (list 'code-print (s#substring rest 1 len)) (list 'code (s#substring rest 0 len))) (cons (list 'text buf) acc)))))) (def! gen-renderer-text (fn* [rest buf acc] (if (nil? rest) (cons (list 'text buf) acc) ; end of iteration (if (s#start-with? rest "<%") (gen-renderer-code (s#drop rest 2) ; "<%" を除去 buf acc) (gen-renderer-text (s#drop rest 1) (str buf (s#first rest)) acc))))) (def! to-code (fn* [part] (let* [ type (nth part 0) content (nth part 1) ] (cond (= type 'code) content (= type 'code-print) (str "(print " content ")\n") (= type 'text) (str "(print " (pr-str content) ")\n"))))) (def! gen-renderer (fn* [template] (let* [reversed-parts (gen-renderer-text template "" ; buf '() ; acc ) ] (l.foldr (fn* [part acc] (str acc (to-code part))) "" reversed-parts)))) </code></pre> <p>軽い用途向けでも、たとえば入力が 1000 文字を越えると動きません、では困るので TCO(末尾呼び出しの最適化)が効くように書く必要があります。理解があやふやだったのですが、今回 mal の TCO のしくみについておさらいして、ある程度書いて慣れることができて良かったです。</p> <h1 id="レンダリング用コードの eval"><a href="#%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E7%94%A8%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE+eval">レンダリング用コードの eval</a></h1> <p>基本的には eval するだけですが、コンテキストは mal の型に合わせて変換してあげる必要があります。Ruby 版だとこんな感じ。</p> <pre><code class="ruby"># ruby/mal.rb def self.to_mal_val(v) case v when Array # List は mal 側で用意されているクラス List.new(v.map { |el| to_mal_val(el) }) when Hash v .to_a .map { |k, _v| [k.to_s, to_mal_val(_v)] # Java版に合わせてキーを String にしている } .to_h else v end end </code></pre> <h1 id="core への追加"><a href="#core+%E3%81%B8%E3%81%AE%E8%BF%BD%E5%8A%A0">core への追加</a></h1> <p>文字列処理をするための最低限の関数として <code>s#first</code>, <code>s#rest</code>, <code>s.cons</code> と、改行なしで print する <code>print</code> を追加しました。</p> <p>Ruby 版であればこんなの。</p> <pre><code class="ruby"># ruby/mal.rb ADDITIONAL_CORE_FUNCS = { :print => lambda { |x| print x }, :"s#first" => lambda { |_self| _self[0] }, :"s#rest" => lambda { |_self| if _self.size <= 1 nil else _self[1..-1] end }, :"s.cons" => lambda { |first, rest| if rest.nil? first else first + rest end } } </code></pre> <p>あとはこれを <code>$core_ns</code> に追加してあげればOK。</p> <pre><code class="ruby"># {mal}/impls/ruby/stepA_mal.rb のこの部分 $core_ns .merge(ADDITIONAL_CORE_FUNCS) </code></pre> <p>他にも便利な関数を追加してリッチにしていくと、mal のレイヤーで書く部分が楽になったりパフォーマンスが改善されたりすると思うのですが、移植コストとのトレードオフになるでしょう。</p> <hr /> <p>というわけで、 mal のコードを一度書くだけ(※1)で 86 の言語(※2)で同じように動く(※3)テンプレートエンジンが手に入りました。</p> <p>※1: 実際は各言語ごとに多少手を入れる必要あり<br /> ※2: 2021-12-25 現在<br /> ※3: たぶん。試してないです。</p> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <ul> <li>動いたので満足(という程度の試みです)</li> <li>2日くらいで書いたプロトタイピングです。細かいとこは雑。</li> <li>自分用のツールなどでは様子を見つつ使っていこうかなという気持ちになった</li> <li>テンプレートエンジン、ミニマムな機能だけでよければ簡単。「試しに何か書いてみたい」というときのお題としてもお手頃で、実用できそうな雰囲気があるのも良いです。</li> <li><code>pr-str</code> は言語依存の機能で実装されている <ul> <li>たとえば Ruby 版は inspect、 Java 版では commons-lang3 の StringEscapeUtils を使っている</li> <li>コーナーケースが気になる場合は mal で書き直すとよさそう</li> </ul></li> <li>遅い。文字列の処理で、一度文字のリストにばらして加工してまた文字列に戻す(<code>s#to-chars</code> と <code>s.from-chars</code>)ということをやっていて、遅いです。<br /> 文字列版の car, cdr, cons だけあればあとはリスト用の関数に丸投げできるよね、というのを(やったことなかったので)試してみたかったのでした。今回やってみて気が済みました。<br /> 直交性はあるけど普通に計算量的に厳しい、という体験ができました。後で書き直すかも。</li> <li>たとえば Java でテンプレートエンジン自作するとなると、コード生成まではいいとして実行どうするんだろ、コンパイルしないといけないよね? <a target="_blank" rel="nofollow noopener" href="https://www.ne.jp/asahi/hishidama/home/tech/java/JavaCompiler.html">JavaCompiler</a><br /> を使えばできそう? とかのあたりがめんどくさそうだなーと思って実際に作るとこまで至っていなかったのですが(やったことないので億劫)、Java で書かれたインタプリタで動かせばコンパイルのことを考えなくてよくなると気付いて、そっか、そういう手があるのか、なるほどとなりました。で、そういうのを思いついたときにサッと使える mal は便利。</li> </ul> <h2 id="Java版だとhashmapのキーとしてシンボルが使えない"><a href="#Java%E7%89%88%E3%81%A0%E3%81%A8hashmap%E3%81%AE%E3%82%AD%E3%83%BC%E3%81%A8%E3%81%97%E3%81%A6%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AB%E3%81%8C%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84">Java版だとhashmapのキーとしてシンボルが使えない</a></h2> <p>詳しく調べてませんがメモ。</p> <p>Java版は hashmap のキーとして文字列を期待しているらしく、エラーになります。</p> <pre><code>Mal [java] user> { 'a 123 } Uncaught java.lang.ClassCastException: mal.types$MalList cannot be cast to mal.types$MalString: mal.types$MalList cannot be cast to mal.types$MalString # 文字列ならOK user> { "a" 123 } {"a" 123} </code></pre> <p>Ruby 版だとシンボルでもOK。</p> <pre><code>Mal [ruby] user> { 'a 123 } {a 123} </code></pre> sonota486 tag:crieit.net,2005:PublicArticle/17878 2021-12-21T07:40:58+09:00 2021-12-21T08:00:24+09:00 https://crieit.net/posts/Bash シェルスクリプト(Bashスクリプト)でかんたんな自作言語のコンパイラを書いた <p>シェルスクリプト Advent Calendar 2021<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2021/shellscript">https://qiita.com/advent-calendar/2021/shellscript</a><br /> の20日目の記事です。</p> <p><a href="https://crieit.now.sh/upload_images/d022495e2d320868f0b13d07c98d3f9861b66f943faf6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d022495e2d320868f0b13d07c98d3f9861b66f943faf6.png?mw=700" alt="image" /></a></p> <p>Bashがある環境ならどこでも自作言語で書いたプログラムをコンパイルできるようになりました。</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-bash">https://github.com/sonota88/vm2gol-v2-bash</a></p> <p>いろいろと雑なのですが、アドベントカレンダーの期日があるのでとりあえず公開します。</p> <p>サイズはこんな感じ。</p> <pre><code>$ wc -l {lexer,parser,codegen}.sh lib/*.sh 220 lexer.sh 655 parser.sh 525 codegen.sh 42 lib/common.sh 193 lib/json.sh 284 lib/utils.sh 1919 合計 </code></pre> <h1 id="動かし方の例"><a href="#%E5%8B%95%E3%81%8B%E3%81%97%E6%96%B9%E3%81%AE%E4%BE%8B">動かし方の例</a></h1> <pre><code class="bash">echo ' func add(a, b) { return a + b; } func main() { var one = 1; var result; call_set result = add(one, 2); } ' | ./lexer.sh | ./parser.sh | ./codegen.sh # ↓アセンブリが出力される call main exit label add push bp cp sp bp cp [bp:2] reg_a push reg_a cp [bp:3] reg_a push reg_a pop reg_b pop reg_a add_ab cp bp sp pop bp ret label main push bp cp sp bp sub_sp 1 cp 1 reg_a cp reg_a [bp:-1] sub_sp 1 cp 2 reg_a push reg_a cp [bp:-1] reg_a push reg_a _cmt call~~add call add add_sp 2 cp reg_a [bp:-2] cp bp sp pop bp ret # (snip) </code></pre> <h1 id="移植元"><a href="#%E7%A7%BB%E6%A4%8D%E5%85%83">移植元</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2">https://github.com/sonota88/vm2gol-v2</a></p> <p>Bashスクリプト版のベースになっているバージョンは <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/tree/62">tag:62</a> のあたり</p> <p><自作言語処理系の説明用テンプレ></p> <p>自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。</p> <ul> <li>小規模: コンパイラ部分は 1,000 行程度</li> <li>pure Ruby / 標準ライブラリ以外のライブラリ不要</li> <li>x86風の自作VM向けにコンパイルする</li> <li>ライフゲームのために必要な機能だけ <ul> <li>変数宣言、代入、反復、条件分岐、関数定義</li> <li>演算子: <code>+</code>, <code>*</code>, <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/vm2gol_v2_additional_features">本体には含めていない後付けの機能など</a> <ul> <li>真偽値リテラル / break / if/else / 単項マイナス / Racc などを使って書いたパーサの別実装</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/132314">Ruby 以外の言語への移植</a> <ul> <li>コンパイラ部分のみ</li> <li>Python, Java, TypeScript など、2021-12-19 の時点では 20言語</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/1e683276541cf1b87b76">セルフホスト版</a></li> <li>製作過程を知りたい場合は<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2019/05/04/234516">製作メモ</a>を見てください</li> </ul> <p><説明用テンプレおわり></p> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <h2 id="文字列"><a href="#%E6%96%87%E5%AD%97%E5%88%97">文字列</a></h2> <p>下記の記事のおかげでなんとかなりました。ありがとうございます 🙏</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/richmikan@github/items/6b763519a28b4ce40031">シェルスクリプトはバイナリを扱えない。さてどうしよう…… - Qiita</a></p> <p>バイト単位で16進数表現との相互変換さえできてしまえば、あとは煮るなり焼くなり思いのまま、です。</p> <h2 id="データ構造"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0">データ構造</a></h2> <pre><code class="json">[ 123, "fdsa", [ 11, "FDSA", [] ] ] </code></pre> <p>のような、リストの要素として整数、文字列、リストを持たせることができる再帰的なデータ構造が必要です。Bashスクリプトの配列は単純な値を入れる分にはいいのですが、ネストしたリストを扱うことができなさそうでした。</p> <p>(※ いつもならこういうことをやりたくなった段階で別の言語で書くことを検討しますが、今回は Bashスクリプトで書くこと自体が目的です)</p> <p>そこで、ひとまず下記のようにしました。もっと良いやり方がある気がします。</p> <p>リストの要素にあたるデータの表現はこんな感じ。</p> <pre><code class="bash"># 整数 "int:-123" # 文字列 "str:fdsa" # リスト "list:1" </code></pre> <ul> <li>先頭に型を表すタグを付けた1行の文字列</li> <li>文字列の値(<code>str:...</code> の <code>...</code> の部分)は改行を含まない(レキサで捨てる)ため、改行についての考慮は不要</li> <li><code>list:</code> の後に続く数字はリストID</li> </ul> <p>たとえば <code>["+", 1, -2]</code> というリストは次のように3行の文字列で表せます。1行がリストの要素1つに対応します。</p> <pre><code class="bash">"str:+ int:1 int:-2 " </code></pre> <p>この文字列をグローバルな配列変数に入れて管理する形にしました。要素のインデックスがリストIDに対応します。</p> <p>たとえば、入れ子のあるリスト <code>["+", 1, ["*", 2, 3]]</code> は次のようになります。</p> <pre><code class="bash"># GLOBAL[0] "str:* int:2 int:3 " # GLOBAL[1] "str:+ int:1 list:0 " </code></pre> <p>パフォーマンスの問題は別途ありますが、要するに下記のような操作ができるインターフェイスが用意できればOK。</p> <ul> <li>リストの要素数を求める</li> <li>リストからn番目の要素を読み書きする</li> <li>リストの要素の型を判別する</li> <li>リストの要素の値を取りだす</li> </ul> <h2 id="関数からの値の返却"><a href="#%E9%96%A2%E6%95%B0%E3%81%8B%E3%82%89%E3%81%AE%E5%80%A4%E3%81%AE%E8%BF%94%E5%8D%B4">関数からの値の返却</a></h2> <p>関数 <code>myfunc</code> から標準出力に出力して <code>result="$(myfunc)"</code> のようにコマンド置換で受け取る方法だと不都合な点が2つあります。</p> <ul> <li>(1) 末尾の改行の有無を制御しにくい <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ko1nksm/items/7c4040c6b6f3fac14db7">シェルスクリプトにはコマンド出力を変数に入れると末尾の改行が全部消えてしまう罠がある! - Qiita</a> に書かれている通り</li> </ul></li> <li>(2) 呼び出した関数内でグローバル変数を更新できない <ul> <li>コマンド置換だとサブシェルになるので</li> </ul></li> </ul> <p>ちょっと悩みましたが、結局下記の方式に落ち着きました。</p> <ul> <li>関数からの戻り値の受け渡し用のグローバル変数 <code>RV1</code> を用意する</li> <li><code>$(...)</code> で囲まずに普通に関数を呼び出す</li> <li>関数側では、返したい値を <code>RV1</code> に代入する</li> <li>呼び出し元では、関数呼び出しの直後に <code>RV1</code> から移し替える</li> </ul> <p>単純な例として、文字列を2つ受け取って連結した文字列を返す関数。</p> <pre><code class="bash"># return value RV1= myfunc() { RV1="${1}${2}" } myfunc "foo" "bar" retval="$RV1" </code></pre> <p>こうですね。単にこのパターンで書けばよいだけなので頭は使わなくてよいです。無心に書いていくとコンパイラができあがります。並行処理とかやってないですし、関数から戻った直後で受け取る約束さえ守っていれば問題は起こりません。</p> <p>全然スマートじゃなくてなんじゃこりゃって感じのソリューションですけど、この方法にも良いところがあって、<code>RV2</code>, <code>RV3</code>, ... と増やすことで複数の値をいくらでも返すことができます。あと、コマンド置換よりはオーバーヘッドが小さいかもしれません(未確認)。</p> <p>関数の中でグローバル変数を書き換える実際の例としてはリストの生成処理があります。ヒープにリスト用の領域を確保して、リストを指し示すポインタを返すようなイメージです。</p> <pre><code class="bash">new_list() { new_gid local self_=$RV1 GLOBAL[$self_]="" RV1=$self_ } new_list list1_=$RV1 </code></pre> <p>Ruby で書くとしたらこんな感じ。</p> <pre><code class="ruby">def new_list self_ = new_gid() # 新しいインデックスを採番 $GLOBAL[self_] = "" return self_ end list_ = new_list() </code></pre> <hr /> <p>というわけで、かんたんなコンパイラを書くことにより、Bashスクリプトにおける</p> <ul> <li>文字列処理</li> <li>データ構造(入れ子のあるリスト)</li> <li>関数からの値の返却</li> </ul> <p>についてのノウハウが新たに得られました。めでたしめでたし。</p> <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%9F%E3%81%B6%E3%82%93%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> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/1482e236054ad3edfdf5">Perlでかんたんな自作言語のコンパイラを書いた</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/54c65e97aa563009137e">なでしこ3でかんたんな自作言語のコンパイラを書いた</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/31c127c42a9722892aa8">素朴な自作言語のコンパイラをLibreOffice Basicに移植した</a></p> sonota486 tag:crieit.net,2005:PublicArticle/17847 2021-12-13T06:56:59+09:00 2021-12-21T07:47:50+09:00 https://crieit.net/posts/nadesiko3-simple-compiler なでしこ3でかんたんな自作言語のコンパイラを書いた <p>日本語プログラミング言語「なでしこ」 Advent Calendar 2021<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2021/nadesiko">https://qiita.com/advent-calendar/2021/nadesiko</a><br /> の13日目の記事です。</p> <p><a href="https://crieit.now.sh/upload_images/d022495e2d320868f0b13d07c98d3f9861b66f943faf6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d022495e2d320868f0b13d07c98d3f9861b66f943faf6.png?mw=700" alt="image" /></a></p> <p>ライフゲームのコンパイルが通ればヨシ、という程度の雑なものです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-nadesiko3">https://github.com/sonota88/vm2gol-v2-nadesiko3</a></p> <h1 id="移植元"><a href="#%E7%A7%BB%E6%A4%8D%E5%85%83">移植元</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2">https://github.com/sonota88/vm2gol-v2</a></p> <p>なでしこ3版のベースになっているバージョンは <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/tree/62">tag:62</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> のみ(優先順位は <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/vm2gol_v2_additional_features">本体には含めていない後付けの機能など</a> <ul> <li>真偽値リテラル / break / if/else / 単項マイナス / Racc などを使って書いたパーサの別実装</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> <li>製作過程を知りたい場合は<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2019/05/04/234516">製作メモ</a>を見てください</li> </ul> <p><説明用テンプレおわり></p> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <p>なでしこ3版のサイズ(行数)はこれくらい。1,000行切ってますね。</p> <pre><code>$ wc -l *.nako3 lib/*.nako3 365 codegen.nako3 110 lexer.nako3 422 parser.nako3 49 lib/utils.nako3 946 合計 </code></pre> <ul> <li>そんなに苦労しなかった <ul> <li>書き味は普通にスクリプト言語っぽい感じ</li> <li>標準でJSON読み書きできる、ネストした配列が作れる</li> <li><code>[1, "a", []]</code> のように JavaScript っぽくリテラルが書ける</li> </ul></li> <li>ファイル分割できない? <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_system%2F%E3%83%8A%E3%83%87%E3%82%B7%E3%82%B3&show">ナデシコ命令</a>を使って分割したファイルの取込み(というか読み込んだコードの評価)ができないか軽く試したのですが、期待した動きにならなかったので、諦めて適当なスクリプト(<code>preproc.rb</code>)で前処理することに</li> </ul></li> </ul> <hr /> <p><code>引数+助詞、引数+助詞、 ... 関数。</code> のように書くのだ、ということをまず最初に知ると基本的な読み書きができるようになります。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E5%9F%BA%E6%9C%AC&show">なでしこさん マニュアル - 文法/基本</a></li> </ul> <blockquote> <p>命令文の基本的な文形 *<br /> そして、なでしこのプログラムの構造は、以下のような形式になっています。<br /> <code>引数+助詞、引数+助詞、 ... 関数。</code><br /> 『「こんにちは」と表示』という命令文で言えば、「こんにちは」が引数で、「表示」が関数となります。</p> </blockquote> <p>「読んで」が <code>読</code> と <code>んで</code> に別れる、みたいなのもあって最初はよく分からなかった気がするので、助詞一覧にも目を通しておくとよいです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E5%AD%97%E5%8F%A5%E8%A7%A3%E6%9E%90&show">なでしこさん マニュアル - 文法/字句解析</a></li> </ul> <p>予約語や助詞はハイライトがあるといいなと思って、 Emacs の generic-mode で色を付けて書いていました。最初は助詞一覧が頭に入ってないので、エディタのサポートがあると入門がスムーズになるように思います。Qiita のコードブロックでもサポートされるといいですね。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/f73841fa-ac65-add9-98f5-ac5492581138.png" alt="0044.png" /></p> <ul> <li>参考: <a target="_blank" rel="nofollow noopener" href="https://qiita.com/tm_tn/items/3b40b5ab5e72750ffd7f">Emacs でオレオレ言語用モードを簡単作成! (ヘルプもあるよ!) - Qiita</a></li> </ul> <hr /> <p>最初は慣れてなくて試行錯誤が少しありました、という例をメモ。</p> <pre><code># 「変数宣言パース」はトークン列をパースして変数宣言文オブジェクトを返す自作関数 # 「文リスト」は配列 変数宣言パースして文リストに配列追加 </code></pre> <p><code>変数宣言パース</code> 関数の戻り値を <code>文リスト</code> に追加したいのですが、これは文法エラーになります。</p> <p>マニュアルの <a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_system%2F%E9%85%8D%E5%88%97%E8%BF%BD%E5%8A%A0&show">「配列追加」命令</a> の説明に書かれているように、<code>AにBを</code> または <code>Aへ</code> の形で引数を渡さなければいけない(関数定義で定められた通りに助詞を添えなければいけない)からです。</p> <p>ふーむ、なるほど、ということで次のように一時変数を補って2行(2文?)に分けると <code>AにBを配列追加</code> の形になり、動くようになります。</p> <pre><code>変数宣言パースして文に代入 文リストに文を配列追加 </code></pre> <p>1行で書けないでしょうか?</p> <pre><code># () はなくてもOK 文リストに(変数宣言パース)を配列追加 </code></pre> <p>1行で書けるようになりました。でもなんか微妙ですね……:thinking:</p> <p>こういう場合は、「それ」を使うと <code>AにBを配列追加</code> の形に持ち込むことができます。</p> <pre><code>変数宣言パースして、文リストにそれを配列追加 # 「Aに」「Bを」の順番は入れ替えてもよい 変数宣言パースして、それを文リストに配列追加 </code></pre> <p>自然になりました。なるほど、こう書けばいいのかー。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E3%81%9D%E3%82%8C&show">なでしこさん マニュアル - 文法/それ</a></li> </ul> <hr /> <p>なでしこ3には敬語があります。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E6%95%AC%E8%AA%9E&show">なでしこさん マニュアル - 文法/敬語</a></li> </ul> <blockquote> <p>なでしこv3.1.14以降、なでしこで礼節をわきまえたプログラムを書くことができるようになりました。</p> </blockquote> <p>使ってみました。なでしこ3では礼節のあるコンパイラを書くことができます。</p> <pre><code>パースして抽象構文木に代入してください。 抽象構文木をJSONエンコードして、それを表示してください。お願いします。 </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%82%82%EF%BC%88%E3%81%9F%E3%81%B6%E3%82%93%EF%BC%89%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/d43c9618498062863e58">なでしこ3で簡単なcatコマンドを作る</a></li> </ul> sonota486 tag:crieit.net,2005:PublicArticle/17844 2021-12-11T12:03:55+09:00 2021-12-11T12:05:57+09:00 https://crieit.net/posts/libreoffice-calc-jruby-sinatra LibreOffice Calcのfodsファイルを読み書きするサンプルをweb API化してみた <p>これは <a target="_blank" rel="nofollow noopener" href="https://adventar.org/calendars/6425">LibreOffice Advent Calendar 2021</a> の11日目の記事です。</p> <p>↓これにちょろっと付け足して web API 化してみただけの記事です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b88c89d763cf872db5a1">JRubyでLibreOffice Calcのfodsファイルを読み書きするサンプル 2021</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>webapi-2021</code> ブランチ</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/libreoffice-jruby-sample/tree/webapi-2021">https://github.com/sonota88/libreoffice-jruby-sample/tree/webapi-2021</a></p> <p>Java 版でやろうかと考えていたのですが、億劫になって JRuby 版でやりました。 JRuby で Sinatra 使った方が速い。</p> <p>主な部分だけ抜き出すとこんな感じ。考えるのが面倒だったので REST ではなく RPC風で。普通に <a target="_blank" rel="nofollow noopener" href="http://sinatrarb.com/">Sinatra</a> を使ってるだけですね。あんまり書くことがない…… 😓</p> <pre><code class="ruby"># app.rb require "sinatra" require_relative "libo_calc" post "/calc" do file = params["file"] sheet_name = params["sheet"] data = {} Calc.open(file) do |doc| sheet = doc.get_sheet_by_name(sheet_name) case params["command"] when "cell_get" data = cell_get(sheet, params) when "cell_set" cell_set(sheet, params) doc.save() when "dump" data = dump(sheet) else raise "unsupported command" end end content_type :json JSON.pretty_generate(data) end </code></pre> <h1 id="動かし方"><a href="#%E5%8B%95%E3%81%8B%E3%81%97%E6%96%B9">動かし方</a></h1> <p>イメージをビルド</p> <pre><code>docker build \ --build-arg USER=$USER \ --build-arg GROUP=$(id -gn) \ -t my:libo-jruby-webapi-2021 . </code></pre> <p>APIサーバ起動</p> <pre><code>./docker_run.sh ./jruby.sh app.rb -o 0.0.0.0 </code></pre> <h1 id="curl で動作確認"><a href="#curl+%E3%81%A7%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">curl で動作確認</a></h1> <p>ファイル、シート名、コマンド、パラメータを POST で渡します。</p> <pre><code class="bash">curl -XPOST 'http://localhost:4567/calc' \ -d 'file=./sample.fods' -d 'sheet=Sheet1' \ -d 'command=cell_set' \ -d 'col=0' -d 'row=2' -d "val=$(date "+%F_%T")" #=> {} curl -XPOST 'http://localhost:4567/calc' \ -d 'file=./sample.fods' -d 'sheet=Sheet1' \ -d 'command=cell_get' \ -d 'col=0' -d 'row=2' #=> { "val": "2021-12-11_11:03:20" } curl -XPOST 'http://localhost:4567/calc' \ -d 'file=./sample.fods' -d 'sheet=Sheet1' \ -d 'command=dump' #=> { "rows": [ [ "(0, 0) 日本語テキスト 2021-01-02 14:34:26 +0900", "b1" ], [ "a2", "(1, 1) 2021-01-02 14:34:26 +0900" ], [ "2021-12-11_11:03:20", "12.34" ], [ "", "0" ] ] } </code></pre> <p>読み書きできてます。</p> <h1 id="Ruby + Faraday"><a href="#Ruby+%2B+Faraday">Ruby + Faraday</a></h1> <p>適当な HTTP クライアントライブラリを使って試してみました。</p> <pre><code class="ruby">require "faraday" puts "cell_set =>" res = Faraday.post( "http://localhost:4567/calc", { file: "./sample.fods", sheet: "Sheet1", command: "cell_set", col: 0, row: 2, val: Time.now.to_s } ) puts res.body puts "cell_get =>" res = Faraday.post( "http://localhost:4567/calc", { file: "./sample.fods", sheet: "Sheet1", command: "cell_get", col: 0, row: 2 } ) puts res.body puts "dump =>" res = Faraday.post( "http://localhost:4567/calc", { file: "./sample.fods", sheet: "Sheet1", command: "dump" } ) puts res.body </code></pre> <pre><code class="bash">$ bundle exec ruby sample_webapi_client.rb cell_set => { } cell_get => { "val": "2021-12-11 11:09:49 +0900" } dump => { "rows": [ [ "(0, 0) 日本語テキスト 2021-01-02 14:34:26 +0900", "b1" ], [ "a2", "(1, 1) 2021-01-02 14:34:26 +0900" ], [ "2021-12-11 11:09:49 +0900", "12.34" ], [ "", "0" ] ] } </code></pre> <p>大丈夫ですね。普通ですね。</p> <hr /> <p>というわけで、任意の HTTP クライアントからセルの読み書きができるようになりました。この記事は以上です。</p> sonota486 tag:crieit.net,2005:PublicArticle/17834 2021-12-07T21:52:40+09:00 2021-12-07T21:52:40+09:00 https://crieit.net/posts/nadesiko3-cat-command なでしこ3で簡単なcatコマンドを作る <p><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/doc3/">なでしこ3</a> に入門しています。</p> <pre><code>$ cnako3 -v 3.2.30 </code></pre> <p>新しくプログラミング言語を覚えるとき、入出力や反復などの基本的な機能の下調べも兼ねて、最近はよく cat コマンドを書いています。なでしこでもやってみます。</p> <ul> <li>参考: <a target="_blank" rel="nofollow noopener" href="https://atmarkit.itmedia.co.jp/ait/articles/1602/25/news034.html">【 cat 】コマンド――設定ファイルの内容を簡単に確認する:Linux基本コマンドTips(1) - @IT</a></li> </ul> <p>ちなみに、なでしこ3は Node.js ベースなので Linux でも使えます。この記事も Docker コンテナ内で動かして書いています。</p> <ul> <li><p><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/doc3/index.php?%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%ABPC%E3%81%A7%E3%83%90%E3%83%83%E3%83%81%E5%87%A6%E7%90%86%E3%81%AB%E4%BD%BF%E3%81%86">ローカルPCでバッチ処理に使う - 日本語プログラミング言語「なでしこ3」</a></p> <ul> <li><code>npm install -g nadesiko3</code> でインストールできます</li> </ul></li> <li><p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kujirahand/items/2a940c14fd2b5ac0db29">Chromebookになでしこをインストールする - Qiita</a></p></li> </ul> <h1 id="標準出力に出力する"><a href="#%E6%A8%99%E6%BA%96%E5%87%BA%E5%8A%9B%E3%81%AB%E5%87%BA%E5%8A%9B%E3%81%99%E3%82%8B">標準出力に出力する</a></h1> <p>まずは基本ということで。</p> <pre><code>「こんにちは」と表示。 </code></pre> <p>これは簡単。チュートリアルの最初に出てきますね。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB&show">なでしこさん マニュアル - チュートリアル</a></li> </ul> <h1 id="標準入力から読む"><a href="#%E6%A8%99%E6%BA%96%E5%85%A5%E5%8A%9B%E3%81%8B%E3%82%89%E8%AA%AD%E3%82%80">標準入力から読む</a></h1> <p>マニュアルで探して <code>標準入力取得時</code> という命令を見つけました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_node%2F%E6%A8%99%E6%BA%96%E5%85%A5%E5%8A%9B%E5%8F%96%E5%BE%97%E6%99%82&show">なでしこさん マニュアル - plugin_node/標準入力取得時</a></li> </ul> <p>使ってみます。</p> <pre><code># cat1.nako3 標準入力取得時には(行) 行を表示 ここまで </code></pre> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E7%84%A1%E5%90%8D%E9%96%A2%E6%95%B0&show">なでしこさん マニュアル - 文法/無名関数 / 「…には」構文 / 暗黙的な無名関数の指定</a></li> </ul> <p>動かしてみます。</p> <pre><code>$ cat cat1.nako3 | cnako3 cat1.nako3 標準入力取得時には(行) 標準入力取得時には(行) 行を表示 行を表示 ここまで ここまで $ </code></pre> <p>各行が2回表示されていますが、これはインストールしたなでしこに含まれている <code>src/plugin_node.js</code> を修正して createInterface のオプションとして <code>terminal: false</code> を指定することで解消できました。</p> <pre><code>$ cat cat1.nako3 | cnako3 cat1.nako3 | cat 標準入力取得時には(行) 行を表示 ここまで $ </code></pre> <p>参考:</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/kujirahand/nadesiko3/blob/3.2.30/src/plugin_node.js">https://github.com/kujirahand/nadesiko3/blob/3.2.30/src/plugin_node.js</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/nodejs/node/issues/30510">readline duplicates input sent from the console/terminal · Issue #30510 · nodejs/node</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://nodejs.org/api/readline.html#readlinepromisescreateinterfaceoptions">Readline | Node.js v17.2.0 Documentation / readlinePromises.createInterface(options)</a></li> </ul> <p>はい、これで cat コマンドっぽいものができました。簡単ですね。</p> <hr /> <p>さすがにこれだけだとさびしいので、もうちょっとあれこれやってみます。</p> <h1 id="末尾に改行を付けない版の「表示」が使いたい"><a href="#%E6%9C%AB%E5%B0%BE%E3%81%AB%E6%94%B9%E8%A1%8C%E3%82%92%E4%BB%98%E3%81%91%E3%81%AA%E3%81%84%E7%89%88%E3%81%AE%E3%80%8C%E8%A1%A8%E7%A4%BA%E3%80%8D%E3%81%8C%E4%BD%BF%E3%81%84%E3%81%9F%E3%81%84">末尾に改行を付けない版の「表示」が使いたい</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://www.nadesi.com/man/index.php?%E7%B6%99%E7%B6%9A%E8%A1%A8%E7%A4%BA">なでしこ v1 には「継続表示」という命令があり</a>、末尾に改行を付けずに出力できるようなのですが、v3 では用意されていないようです(v3.2.30 時点)。</p> <p>cat コマンドの基本の動作としては入力の内容を変えずにそのまま出力したいのですが、「表示」命令だと末尾に改行がない入力が来た場合に内容が変わってしまいます。</p> <p>そこで、「JSメソッド実行」という命令を利用して「継続表示」関数を作ってみました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_system%2FJS%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%AE%9F%E8%A1%8C&show">なでしこさん マニュアル - plugin_system/JSメソッド実行</a></li> </ul> <pre><code>●(値を)継続表示とは 「process.stdout」の「write」を["{値}"]でJSメソッド実行 ここまで 123を継続表示 「継続表示の」を継続表示 「テスト」を継続表示 「です。{LF}」を継続表示 </code></pre> <pre><code>$ cnako3 sample_keizoku_hyouji.nako3 123継続表示のテストです。 $ </code></pre> <p>よしよし。ちなみに <code>stdout</code> を <code>stderr</code> にすれば標準エラー出力に出力できます。</p> <p>このように、なでしこ自体に命令が用意されていない場合でも JavaScript(Node.js)の機能を使うことで自分でなんとかすることができます。</p> <p>この例では「JSメソッド実行」を使いましたが、他にも似た命令として「JS実行」などがあります。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_system%2F%E7%89%B9%E6%AE%8A%E5%91%BD%E4%BB%A4&show">なでしこさん マニュアル - plugin_system/特殊命令</a></li> </ul> <hr /> <p>余談ですが eval がありますね!</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_system%2F%E3%83%8A%E3%83%87%E3%82%B7%E3%82%B3&show">なでしこさん マニュアル - plugin_system/ナデシコ</a></li> </ul> <p>↓のようなことができるそうです。</p> <pre><code>AAA=0 『AAA=123』をナデシコする AAAを表示 </code></pre> <h1 id="標準入力から全部読みたい"><a href="#%E6%A8%99%E6%BA%96%E5%85%A5%E5%8A%9B%E3%81%8B%E3%82%89%E5%85%A8%E9%83%A8%E8%AA%AD%E3%81%BF%E3%81%9F%E3%81%84">標準入力から全部読みたい</a></h1> <p><code>標準入力取得時</code> だと <code>EOF</code> (入力ストリームの終端)が検出できないため、「標準入力からとりあえず全部読んで何かしたい」「標準入力から読み終わったタイミングで何かしたい」といったことができないようでした。</p> <p>なでしこの機能だけで簡単に済ます方法を思いつけなかったので、標準入力からの入力をシェルスクリプトで一時ファイルに出力して、なでしこではその一時ファイルから読む、という方式にしてみました。<br /> ファイル名は <code>stdin</code> で決め打ち。</p> <pre><code class="sh">#!/bin/bash STDIN_FILE=stdin if [ -e $STDIN_FILE ]; then rm $STDIN_FILE fi if [ -p /dev/stdin ]; then # 標準入力の内容をすべてファイルに保存 cat > $STDIN_FILE fi cnako3 "$@" </code></pre> <p>これを <code>cnako3.sh</code> として保存して、以降は <code>cnako3</code> コマンドを置き換える形で</p> <pre><code>$ ls | ./cnako3.sh sample.nako3 </code></pre> <p>のように使っていきます。</p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://qiita.com/xtetsuji/items/d9800864c23af47c1443">標準入力にデータを与えられているかどうか調べる方法 - Qiita</a></p> <p>なでしこ側では普通にファイルを読めばよいですね。</p> <pre><code># cat2.nako3 ●標準入力全部読とは もし、「stdin」が存在するならば 「stdin」を読んで戻す 違えば 「標準入力からの読み込みに失敗」のエラー発生 ここまで ここまで 標準入力全部読んでテキストに代入 テキストを表示 </code></pre> <p><code>標準入力全部読んでテキストに代入</code> したあとは、普通の文字列の処理です。</p> <pre><code>$ cat sample_end_with_newline.txt | ./cnako3.sh cat2.nako3 UTF-8 テキストファイル ファイル終端の改行あり 行末にスペース→ $ </code></pre> <h1 id="改造してみる"><a href="#%E6%94%B9%E9%80%A0%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">改造してみる</a></h1> <p>cat コマンドなんか作って何が面白いんや? と思われるかもしれませんが、 cat ができるとそれを改造していろいろ遊べます。</p> <h2 id="LF を &quot;<改行>&quot; + LF に置換してファイル終端も表示"><a href="#LF+%E3%82%92+%26quot%3B%EF%BC%9C%E6%94%B9%E8%A1%8C%EF%BC%9E%26quot%3B+%2B+LF+%E3%81%AB%E7%BD%AE%E6%8F%9B%E3%81%97%E3%81%A6%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E7%B5%82%E7%AB%AF%E3%82%82%E8%A1%A8%E7%A4%BA">LF を "<改行>" + LF に置換してファイル終端も表示</a></h2> <pre><code># cat3.nako3 # 継続表示、標準入力全部読 は同じなので省略 標準入力全部読んでテキストに代入 テキストのLFを「<改行>{LF}」に置換して継続表示 「<ファイル終端>」を表示 </code></pre> <p>入力の最後に改行がある場合:</p> <pre><code>$ cat sample_end_with_newline.txt | ./cnako3.sh cat3.nako3 UTF-8 テキストファイル<改行> ファイルの最後に改行あり<改行> 行末にスペース→ <改行> <ファイル終端> $ </code></pre> <p>入力の最後に改行がない場合:</p> <pre><code>$ cat sample_end_without_newline.txt | ./cnako3.sh cat3.nako3 UTF-8 テキストファイル<改行> ファイルの最後に改行なし<改行> 行末にスペース→ <ファイル終端> $ </code></pre> <p>これで行末の余分なスペースを見つけやすくなりますね!<br /> <code>cat --show-all</code> みたいな感じ。<br /> 最後に <code><ファイル終端></code> を付けて、最後の改行やスペースがあるかどうかも分かりやすくしてみました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_system%2F%E7%BD%AE%E6%8F%9B&show">なでしこさん マニュアル - plugin_system/置換</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?plugin_system%2F%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E5%AE%9A%E6%95%B0&show">なでしこさん マニュアル - plugin_system/システム定数</a> <ul> <li><code>LF</code> はシステム定数として定義されています</li> </ul></li> </ul> <h2 id="行番号を表示"><a href="#%E8%A1%8C%E7%95%AA%E5%8F%B7%E3%82%92%E8%A1%A8%E7%A4%BA">行番号を表示</a></h2> <p><code>cat -n</code> みたいなの。</p> <pre><code># cat4.nako3 # 継続表示、標準入力全部読 は同じなので省略 標準入力全部読んでテキストに代入 テキストをLFで区切って行リストに代入 行数 = 行リストの配列要素数 行番号を1から(行数)まで繰り返す   「{行番号}: {行リスト@(行番号 - 1)}」を表示 ここまで </code></pre> <pre><code>$ cat sample_end_with_newline.txt | ./cnako3.sh cat4.nako3 1: UTF-8 テキストファイル 2: ファイルの最後に改行あり 3: 行末にスペース→ 4: $ </code></pre> <p>行番号が分かってべんり!</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E9%85%8D%E5%88%97%E5%A4%89%E6%95%B0&show">なでしこさん マニュアル - 文法/配列変数</a></li> </ul> <h2 id="1文字ずつ処理"><a href="#1%E6%96%87%E5%AD%97%E3%81%9A%E3%81%A4%E5%87%A6%E7%90%86">1文字ずつ処理</a></h2> <p>1文字ずつ処理するバージョンも書いてみました。</p> <pre><code># cat5.nako3 # 継続表示、標準入力全部読 は同じなので省略 ●(テキストから位置の)文字取得 文字配列 = テキストを文字列分解 文字配列@位置を戻す ここまで 標準入力全部読んでテキストに代入 テキスト文字数はテキストの文字数 位置は0 位置が(テキスト文字数 - 1)以下の間繰り返す テキストから位置の文字取得して文字に代入 文字を継続表示 位置 = 位置 + 1 ここまで </code></pre> <p>「文字取得」関数を呼び出すたびに文字列分解していて効率悪いですが、おためしなので気にしない。</p> <pre><code>$ cat sample_end_with_newline.txt | ./cnako3.sh cat5.nako3 UTF-8 テキストファイル ファイルの最後に改行あり 行末にスペース→ $ </code></pre> <h2 id="行をソート"><a href="#%E8%A1%8C%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88">行をソート</a></h2> <p>cat コマンドを改造すると、たとえば行をソートするコマンドなんかも作れるようになります。</p> <pre><code># sort.nako3 # 継続表示、標準入力全部読 は同じなので省略 標準入力全部読んでテキストに代入 テキストをLFで区切って行リストに代入 行リストを配列ソートしてソート済行リストに代入 ソート済行リストを反復 対象を表示 ここまで </code></pre> <pre><code>$ cat sort.nako3 | ./cnako3.sh sort.nako3 「stdin」を読んで戻す 「標準入力からの読み込みに失敗」のエラー発生 「process.stdout」の「write」を["{値}"]でJSメソッド実行 ここまで もし、「stdin」が存在するならば 対象を表示 違えば ●標準入力全部読とは ●(値を)継続表示とは ここまで ここまで ここまで ソート済行リストを反復 テキストをLFで区切って行リストに代入 標準入力全部読んでテキストに代入 行リストを配列ソートしてソート済行リストに代入 $ </code></pre> <hr /> <p>というわけで cat コマンドっぽいものを作っていろいろ試してみました。この記事は以上です。</p> <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://memo88.hatenablog.com/entry/2020/09/19/084602">Zig: 1バイトごとに読み書きするだけのcatコマンドを書いてみた</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/07/18/154534">Deno: 標準入力を読んで行ごとに処理</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20200711_kotlin_read_stdin_each_line">Kotlin: 標準入力を読んで行ごとに処理</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/17680 2021-09-18T09:58:36+09:00 2022-07-10T10:55:15+09:00 https://crieit.net/posts/simple-compiler-php PHPでかんたんな自作言語のコンパイラを書いた <p><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/18/193844">ブログに書いていたもの</a>を引っ越してきました。元の記事公開日は 2020-09-18 です。</p> <hr /> <p>やっつけなので汚いです。ライフゲームのコンパイルが通ったのでヨシ、という程度の雑なものです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-php">https://github.com/sonota88/vm2gol-v2-php</a></p> <h1 id="移植元"><a href="#%E7%A7%BB%E6%A4%8D%E5%85%83">移植元</a></h1> <p>自作言語の概要などについてもこちらを参照してください。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/05/04/155425">Rubyで素朴な自作言語のコンパイラを作った</a></li> </ul> <p>大元は Ruby 版なのですが、実質的には <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/08/053329">Perl版</a>からの移植です。</p> <p>ベースになっているバージョン: <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/tree/50">tag:50</a> のあたり<br /> (追記 2021-09-18: <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20210911_vm2gol_v2_60_pass_stmt">ステップ60</a> の修正まで適用しました)</p> <p>作り方はここに全部書いています(Ruby 版のものですが): <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2019/05/04/234516">vm2gol v2 製作メモ</a></p> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <p>主な部分のサイズ(2021-09-18 現在):</p> <pre><code>$ wc -l {lexer,parser,codegen}.php lib/utils.php 66 lexer.php 536 parser.php 423 codegen.php 42 lib/utils.php 1067 合計 </code></pre> <hr /> <ul> <li>PHP を書くのは今回が初めて</li> <li>さらっと文法を見た感じ Perl に近そうだったので、<br /> Perl版をコピーして書き換えていった <ul> <li>割と機械的に置き換え</li> <li>my を消して</li> <li>sub => function</li> <li>elsif => elseif</li> <li>正規表現はダブルクォートで囲んで preg_match にして</li> <li>……などなど</li> <li>Perl → PHP という流れで進んだのは良かった</li> </ul></li> <li>Emacs に php-mode を追加するのがめんどくさかったので perl-mode で書いてた</li> <li>クラスも普通に使えるし、 int と string の型の判別もできるし、 Perl より楽ちんという印象。<br /> 正直なところあまり書くことがないです……。</li> </ul> <hr /> <p>今回の実験コーナー。</p> <p>これまで <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/22/162201">Dart への移植</a>のときに set を不要にし、<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/121437">Java への移植</a>のときに call を不要にし、<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/08/053329">Perl への移植</a>のときに call_set を不要にしてきました。</p> <p>これらを全部適用するとどうなるか <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-php/tree/trial">trial ブランチ</a><br /> でやってみました。</p> <p>とりあえず <code>parse_stmt()</code> の分岐のとこだけ貼ってみます。</p> <pre><code class="php"><?php # ... # if ($t->str_eq("set" )) { return parse_set(); } # if ($t->str_eq("call" )) { return parse_call(); } # elseif ($t->str_eq("call_set")) { return parse_call_set(); } elseif ($t->str_eq("return" )) { return parse_return(); } elseif ($t->str_eq("while" )) { return parse_while(); } elseif ($t->str_eq("case" )) { return parse_case(); } elseif ($t->str_eq("_cmt" )) { return parse_vm_comment(); } else { if ( $t->kind_eq("ident") && peek(1)->is("sym", "=") ) { if ( peek(2)->kind_eq("ident") && peek(3)->is("sym", "(") ) { return parse_call_set(); } else { return parse_set(); } } elseif ( $t->kind_eq("ident") && peek(1)->is("sym", "(") ) { return parse_call(); } else { throw not_yet_impl($t); } } </code></pre> <p>個別に試していたときはそれぞれの個別のことだけ考えてやればよかったのが、3つを同時に満たそうとして else に押し込めてとりあえずこうなった、というものです。ちょっと野暮ったいですが <code>peek(n)</code> で先読みしてやればなんとかなるようです。</p> <p>funcall を expr に含めるとかするともうちょっとすっきりする気がします。</p> <p>vgコードはこんな感じ。ここまでやるとかなり普通の言語っぽい(というか JavaScript っぽい)見た目になりますね。</p> <pre><code class="javascript">func main() { var w = 5; // 盤面の幅 var h = 5; // 盤面の高さ // 初期状態の設定 vram_set(w, 1, 0, 1); vram_set(w, 2, 1, 1); vram_set(w, 0, 2, 1); vram_set(w, 1, 2, 1); vram_set(w, 2, 2, 1); var gen_limit = 0; var gen = 1; while (gen != gen_limit) { make_next_gen(w, h); replace_with_buf(); gen = gen + 1; } } </code></pre> <h1 id="他の言語への移植"><a href="#%E4%BB%96%E3%81%AE%E8%A8%80%E8%AA%9E%E3%81%B8%E3%81%AE%E7%A7%BB%E6%A4%8D">他の言語への移植</a></h1> <p>| <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/18/193844">PHP</a> | <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-php">github</a> | 2020-09-18 |<br /> --></p> <div class="table-responsive"><table> <thead> <tr> <th>記事</th> <th>リポジトリ</th> <th>日付</th> </tr> </thead> <tbody> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20210628_vm2gol_v2_haskell">Haskell</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-haskell">github</a></td> <td>2021-06-28</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2021/06/26/054540">OCaml</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-ocaml">github</a></td> <td>2021-06-26</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/4d446b778ffbbaab267c">Pascal</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-pascal">github</a></td> <td>2021-05-22</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b80696f23a08c3c2fc5d">Julia</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-julia">github</a></td> <td>2021-05-03</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20210407_vm2gol_v2_rust">Rust</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-rust">github</a></td> <td>2021-04-07</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/275c2b5407986b100cc8">Crystal</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-crystal">github</a></td> <td>2021-03-27</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/1e683276541cf1b87b76">Pric(セルフホスト)</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/pric">github</a></td> <td>2021-02-21</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2021/01/14/032813">Kotlin</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-kotlin">github</a></td> <td>2021-01-14</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2021/01/07/235019">Zig</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-zig">github</a></td> <td>2021-01-07</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/31c127c42a9722892aa8">LibreOffice Basic</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-libreoffice-basic">github</a></td> <td>2020-12-14</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/25/073200">Go</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-go">github</a></td> <td>2020-09-25</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/13/133735">C♭</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-cflat">github</a></td> <td>2020-09-13</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/08/053329">Perl</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-perl">github</a></td> <td>2020-09-08</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/06/043607">C</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-c">github</a></td> <td>2020-09-06</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/121437">Java</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-java">github</a></td> <td>2020-08-30</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/22/162201">Dart</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-dart">github</a></td> <td>2020-08-22</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/5267dcd4750235680c8f">Python</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-python">github</a></td> <td>2020-08-19</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/15/114754">TypeScript (Deno)</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-typescript/tree/20200815">github</a></td> <td>2020-08-15</td> </tr> </tbody> </table></div> sonota486 tag:crieit.net,2005:PublicArticle/17672 2021-09-14T07:28:41+09:00 2021-09-14T20:06:20+09:00 https://crieit.net/posts/simple-compiler-perl Perlでかんたんな自作言語のコンパイラを書いた <p><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/08/053329">ブログに書いていたもの</a>を引っ越してきました。元の記事公開日は 2020-09-08 です。</p> <hr /> <p>20年ぶりくらいに Perl のコードを書きました。やっつけなので汚いです。ライフゲームのコンパイルが通ったのでヨシ、という程度の雑なものです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-perl">https://github.com/sonota88/vm2gol-v2-perl</a></p> <hr /> <h1 id="移植元"><a href="#%E7%A7%BB%E6%A4%8D%E5%85%83">移植元</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/05/04/155425">Rubyで素朴な自作言語のコンパイラを作った</a><br /> (自作言語の概要などについてはこちらを参照してください)</p> <p>ベースになっているバージョン: <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/tree/49">tag:49</a> のあたり<br /> (追記 2021-09-11: ステップ60の修正まで適用しました)</p> <p>作り方はここに全部書いています(Ruby 版のものですが): <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2019/05/04/234516">vm2gol v2 製作メモ</a></p> <p>ただ、Ruby 版よりは主に <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/06/043607">C言語版</a> を見ながら書き写していました。クラスを使わなかったのもあり、C言語版に近いところも多いです。</p> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <ul> <li>20年ぶりくらいといっても、当時はちょっと入門した程度だったしもうほとんど忘れてるので<br /> ほぼゼロから再入門といった感じ</li> <li>でも 2日(日曜+月曜)でなんとか完成した。殴り書きです。</li> <li>配列・ハッシュとリファレンスが絡むあたりが最初よく分からなかった <ul> <li>まず配列の要素数の取り方が分からないとか、<code>for my $x (@$xs) { ... }</code> の <code>@</code> がなくて動かないとか、たぶん初心者あるある</li> </ul></li> <li>数と文字列の区別がない(シェルスクリプトっぽい)のをどうするかちょっと悩んだ。データの型を見て処理を分岐しているところがあるので。 <ul> <li>ハッシュでラッパーオブジェクトみたいなものを作ってどうにかした( <code>lib/Val.pm</code> )。</li> <li>最初は C言語版のときの NodeList, NodeItem 構造体を使う書き方にすれば何も考えずに機械的に移植できそうと思ったけど、あれはあれでリストの処理が煩雑・大げさで、配列で済むならその方がシンプルなので、最低限文字列と数だけ判別できるようにした。</li> </ul></li> <li>クラスの使い方もちょっと調べてみたけど、 もうちょっと Perl 力が上がった状態で使わないと逆に回り道っぽくなりそうな気がして、 C言語版の「構造体+関数群のセット」方式でやってしまった</li> </ul> <hr /> <p><code>my $foo = ...</code> をいっぱい書かないといけなくてさすがに面倒だったので途中で次のような Emacs Lisp を書いて <code>Ctrl + Alt + M</code> で入力できるようにしたり。yasnippet でも良かったかも。</p> <pre><code class="lisp">(add-hook 'perl-mode-hook '(lambda () (local-set-key (kbd "C-M-m") (lambda () (interactive) (insert "my $ = ;") (backward-char 4))))) </code></pre> <hr /> <p><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/22/162201">Dart版</a>で <code>call</code>、<a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/121437">Java版</a>で <code>set</code> を不要にした流れで、今回は <code>call_set</code> キーワードを不要にしてみました。同じ要領でできますね、ということが分かりました。</p> <p>(追記 2021-04-09: この修正はいったん revert しましたが、一応 <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-perl/tree/trial">trial ブランチ</a>に残してあります。)</p> <pre><code class="perl">sub parse_stmt { my $t = peek(0); if (Token::is($t, "sym", "}")) { return -1; } if (Token::str_eq($t, "func" )) { return parse_func(); } elsif (Token::str_eq($t, "var" )) { return parse_var(); } elsif (Token::str_eq($t, "set" )) { return parse_set(); } elsif (Token::str_eq($t, "call" )) { return parse_call(); } # elsif (Token::str_eq($t, "call_set")) { return parse_call_set(); } elsif (Token::str_eq($t, "return" )) { return parse_return(); } elsif (Token::str_eq($t, "while" )) { return parse_while(); } elsif (Token::str_eq($t, "case" )) { return parse_case(); } elsif (Token::str_eq($t, "_cmt" )) { return parse_vm_comment(); } else { if (Token::kind_eq($t, "ident")) { return parse_call_set(); } else { p_e("parse_stmt", $t); die "not_yet_impl"; } } } </code></pre> <hr /> <p>(追記 2021-08-30: その後の修正により、下記の部分はなくなりました)</p> <p>それと、今回はコード生成処理のアレ、何度も似たようなのが出てきて鬱陶しかった部分をためしにサブルーチンに抽出して共通化してみました。うーん。どうでしょう。どうしようかな。とりあえず名前が微妙。</p> <p>名前が微妙な上に上手い抽象でもない気がする……となるとこれはメソッド抽出すべきではないパターンのように思えます。いい解決法が見つかるまで本家の Ruby版には取り込めなさそう。とはいえコーディングの手間・退屈さは減らせるので、移植版では気軽に使っていこうかなと。</p> <pre><code class="perl">sub to_asm_str { my $fn_arg_names = shift; my $lvar_names = shift; my $val = shift; if (Utils::is_arr($val)) { return undef; } elsif (Val::kind_eq($val, "int")) { return $val->{"val"}; } elsif (Val::kind_eq($val, "str")) { my $str = $val->{"val"}; if (0 <= str_arr_index($fn_arg_names, $str)) { return to_fn_arg_ref($fn_arg_names, $str); } elsif (0 <= str_arr_index($lvar_names, $str)) { return to_lvar_ref($lvar_names, $str); } else { return undef; } } else { return undef; } } </code></pre> <p>呼び出し箇所は6箇所。それなりに使いまわせているようではあります。</p> <pre><code>vgcg.pl:132: $push_arg = to_asm_str($fn_arg_names, $lvar_names, $val); vgcg.pl:240: $push_arg = to_asm_str($fn_arg_names, $lvar_names, $fn_arg); vgcg.pl:304: $src_val = to_asm_str($fn_arg_names, $lvar_names, $expr); vgcg.pl:316: my $vram_ref = to_asm_str($fn_arg_names, $lvar_names, sval($vram_arg)); vgcg.pl:342: my $vram_ref = to_asm_str($fn_arg_names, $lvar_names, sval($vram_arg)); vgcg.pl:375: my $vram_ref = to_asm_str([], $lvar_names, sval($vram_arg)); </code></pre> <h1 id="他の言語への移植"><a href="#%E4%BB%96%E3%81%AE%E8%A8%80%E8%AA%9E%E3%81%B8%E3%81%AE%E7%A7%BB%E6%A4%8D">他の言語への移植</a></h1> <div class="table-responsive"><table> <thead> <tr> <th>記事</th> <th>リポジトリ</th> <th>日付</th> </tr> </thead> <tbody> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20210628_vm2gol_v2_haskell">Haskell</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-haskell">github</a></td> <td>2021-06-28</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2021/06/26/054540">OCaml</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-ocaml">github</a></td> <td>2021-06-26</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/4d446b778ffbbaab267c">Pascal</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-pascal">github</a></td> <td>2021-05-22</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/b80696f23a08c3c2fc5d">Julia</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-julia">github</a></td> <td>2021-05-03</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20210407_vm2gol_v2_rust">Rust</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-rust">github</a></td> <td>2021-04-07</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/275c2b5407986b100cc8">Crystal</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-crystal">github</a></td> <td>2021-03-27</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/1e683276541cf1b87b76">Pric(セルフホスト)</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/pric">github</a></td> <td>2021-02-21</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2021/01/14/032813">Kotlin</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-kotlin">github</a></td> <td>2021-01-14</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2021/01/07/235019">Zig</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-zig">github</a></td> <td>2021-01-07</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/31c127c42a9722892aa8">LibreOffice Basic</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-libreoffice-basic">github</a></td> <td>2020-12-14</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/25/073200">Go</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-go">github</a></td> <td>2020-09-25</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/18/193844">PHP</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-php">github</a></td> <td>2020-09-18</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/13/133735">C♭</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-cflat">github</a></td> <td>2020-09-13</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/06/043607">C</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-c">github</a></td> <td>2020-09-06</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/121437">Java</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-java">github</a></td> <td>2020-08-30</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/22/162201">Dart</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-dart">github</a></td> <td>2020-08-22</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/5267dcd4750235680c8f">Python</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-python">github</a></td> <td>2020-08-19</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/15/114754">TypeScript (Deno)</a></td> <td><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-typescript/tree/20200815">github</a></td> <td>2020-08-15</td> </tr> </tbody> </table></div> sonota486 tag:crieit.net,2005:PublicArticle/17631 2021-09-03T06:10:17+09:00 2021-09-03T06:10:17+09:00 https://crieit.net/posts/java-Maven-CompilationFailureException-Compilation-failure (solved) Maven: CompilationFailureException: Compilation failure <p><code>JAVA_HOME</code> が設定されていなかったというありがちなやつですが、次に同じことが起こったときに手間取らないように覚書。</p> <h1 id="状況"><a href="#%E7%8A%B6%E6%B3%81">状況</a></h1> <p>対象: <a target="_blank" rel="nofollow noopener" href="https://pdfbox.apache.org/">Apache PDFBox</a>(<a target="_blank" rel="nofollow noopener" href="https://github.com/apache/pdfbox">GitHub mirror</a>) 2.0.21</p> <p>README にしたがって <code>mvn clean install</code> を実行するとコンパイルで失敗する。</p> <p>スタックトレース:</p> <pre><code>[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.6.0:compile (default-compile) on project fontbox: Compilation failure -> [Help 1] org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.6.0:compile (default-compile) on project fontbox: Compilation failure at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:215) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81) at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56) at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192) at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105) at org.apache.maven.cli.MavenCli.execute (MavenCli.java:972) at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:293) at org.apache.maven.cli.MavenCli.main (MavenCli.java:196) at sun.reflect.NativeMethodAccessorImpl.invoke0 (Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke (Method.java:498) at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282) at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225) at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406) at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347) Caused by: org.apache.maven.plugin.compiler.CompilationFailureException: Compilation failure at org.apache.maven.plugin.compiler.AbstractCompilerMojo.execute (AbstractCompilerMojo.java:1033) at org.apache.maven.plugin.compiler.CompilerMojo.execute (CompilerMojo.java:137) at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo (DefaultBuildPluginManager.java:137) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:210) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81) at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56) at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192) at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105) at org.apache.maven.cli.MavenCli.execute (MavenCli.java:972) at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:293) at org.apache.maven.cli.MavenCli.main (MavenCli.java:196) at sun.reflect.NativeMethodAccessorImpl.invoke0 (Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke (Method.java:498) at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282) at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225) at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406) at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347) </code></pre> <h1 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h1> <pre><code>Ubuntu 18.04 openjdk version "1.8.0_292" (openjdk-8-jdk:amd64) Apache Maven 3.8.2 maven-compiler-plugin 3.6.0 </code></pre> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <p><code>-X (--debug)</code> オプションを付けて実行すると詳しい情報が出力される。<br /> 今回ヒントになったのはこのあたり:</p> <pre><code>$ mvn -X clean install (snip) [INFO] --- maven-compiler-plugin:3.6.0:compile (default-compile) @ fontbox --- (snip) [DEBUG] Configuring mojo 'org.apache.maven.plugins:maven-compiler-plugin:3.6.0:compile' with basic configurator --> [DEBUG] (f) basedir = (snip)/pdfbox/fontbox [DEBUG] (f) buildDirectory = (snip)/pdfbox/fontbox/target [DEBUG] (f) compilePath = [(snip)/pdfbox/fontbox/target/classes, /home/(USER)/.m2/repository/commons-logging/commons-logging/1.2/commons-logging-1.2.jar] [DEBUG] (f) compileSourceRoots = [(snip)/pdfbox/fontbox/src/main/java] [DEBUG] (f) compilerId = javac [DEBUG] (f) debug = true [DEBUG] (f) encoding = UTF-8 [DEBUG] (f) executable = ${env.JAVA_HOME}/bin/javac [DEBUG] (f) failOnError = true [DEBUG] (f) failOnWarning = false [DEBUG] (f) forceJavacCompilerUse = false [DEBUG] (f) fork = true [DEBUG] (f) generatedSourcesDirectory = (snip)/pdfbox/fontbox/target/generated-sources/annotations [DEBUG] (f) mojoExecution = org.apache.maven.plugins:maven-compiler-plugin:3.6.0:compile {execution: default-compile} [DEBUG] (f) optimize = false [DEBUG] (f) outputDirectory = (snip)/pdfbox/fontbox/target/classes [DEBUG] (f) project = MavenProject: org.apache.pdfbox:fontbox:2.0.21 @ (snip)/pdfbox/fontbox/pom.xml [DEBUG] (f) projectArtifact = org.apache.pdfbox:fontbox:bundle:2.0.21 [DEBUG] (f) session = org.apache.maven.execution.MavenSession@31464a43 [DEBUG] (f) showDeprecation = true [DEBUG] (f) showWarnings = false [DEBUG] (f) skipMultiThreadWarning = false [DEBUG] (f) source = 1.6 [DEBUG] (f) staleMillis = 0 [DEBUG] (f) target = 1.6 [DEBUG] (f) useIncrementalCompilation = true [DEBUG] (f) verbose = false [DEBUG] -- end configuration -- [DEBUG] Using compiler 'javac'. (snip) [DEBUG] Excutable: [DEBUG] ${env.JAVA_HOME}/bin/javac [DEBUG] Command line options: [DEBUG] (snip) -target 1.6 -source 1.6 -encoding UTF-8 (snip) </code></pre> <p>javac のパスが</p> <pre><code>[DEBUG] (f) executable = ${env.JAVA_HOME}/bin/javac </code></pre> <p>となっていて、環境変数 <code>JAVA_HOME</code> が期待されているようだったので、</p> <pre><code>JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 mvn clean install </code></pre> <p>のように指定したらコンパイルできるようになった。</p> <p>バージョンが 1.6 となっているのも気にはなって、たぶん正しく指定した方がいいんだろうけどそっちは修正しなくてもよかった。理由は不明。</p> <hr /> <p>ついでにメモ。</p> <p><code>compile</code> ゴールのヘルプ表示コマンド:</p> <pre><code>mvn compiler:help -Ddetail=true -Dgoal=compile </code></pre> sonota486 tag:crieit.net,2005:PublicArticle/17423 2021-06-21T19:23:12+09:00 2022-09-03T10:49:49+09:00 https://crieit.net/posts/pric-self-hosting-compiler 素朴な自作言語Ruccolaのコンパイラをセルフホストした <p>自作言語 Ruccola のコンパイラがセルフホストできました!</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/ruccola/tree/main/selfhost">https://github.com/sonota88/ruccola/tree/main/selfhost</a></p> <p>※ <a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/1e683276541cf1b87b76">Qiita に書いた記事</a>のクロス投稿です</p> <p>※ 最初は Pric という名前だったのですが、 <strong>Ruccola</strong> に変更しました。以下、修正できていない箇所は読み替えてください。</p> <h1 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h1> <h2 id="これまでのあらすじ"><a href="#%E3%81%93%E3%82%8C%E3%81%BE%E3%81%A7%E3%81%AE%E3%81%82%E3%82%89%E3%81%99%E3%81%98">これまでのあらすじ</a></h2> <p>これまで vm2gol(virtual machine to game of life)というプロジェクト名で以下のようなことをやってきました。</p> <ul> <li>(2018) <a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/f9cb3fc4a496b354b729">RubyでオレオレVMとアセンブラとコード生成器を2週間で作ってライフゲームを動かした話</a> <ul> <li>vm2gol-v1</li> <li>高水準言語の代わりに構文木を直接書き、それをコンパイルして VM で動かすところまで</li> <li>ライフゲームだけコンパイル + 実行できればOK</li> <li>雑、汚い</li> </ul></li> <li>(2019〜) <a target="_blank" rel="nofollow noopener" href="https://qiita.com/sonota88/items/2b95378b43a22109513c">Rubyで素朴な自作言語のコンパイラを作った</a> <ul> <li>vm2gol-v2</li> <li>v1 のリライト版(機能的にはほぼ同じ)にフロントエンド部分を追加したもの</li> </ul></li> <li>(2020〜) <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/132314">vm2gol-v2 をいろんな言語に移植</a> <ul> <li>アセンブラ・VM は除いてコンパイラ部分のみ</li> <li>TypeScript / Python / Dart / Java / C / Perl / C♭ / PHP / Go / LibreOffice Basic / Zig / Kotlin / Crystal / Rust / Julia / Pascal / OCaml ... 気が向けば続く予定</li> </ul></li> </ul> <p>今回のセルフホスト編は何もないところから全部書いたわけではなく、すでに v2 がある状態から出発しています。</p> <h2 id="概観"><a href="#%E6%A6%82%E8%A6%B3">概観</a></h2> <p><a href="https://crieit.now.sh/upload_images/911326a328169de98219750f55d6303a60d066da1922d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/911326a328169de98219750f55d6303a60d066da1922d.png?mw=700" alt="image" /></a></p> <p>※ 独自VM向けなので「仮想機械語」と呼ぶべきかもしれませんが、この記事では単に「機械語」とします。<br /> ※ 図ではコンパイラが直接機械語を出力しているように見えますが、実際はアセンブルの過程があります。説明の簡略化のために省略しています。</p> <hr /> <p>T図式でも描いてみました。図の「Pric M」は Pric VM 向けの機械語。</p> <p><a href="https://crieit.now.sh/upload_images/27b190e50a15a1327a9c03e29272adab60d06733795cc.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/27b190e50a15a1327a9c03e29272adab60d06733795cc.png?mw=700" alt="image" /></a></p> <h2 id="期間"><a href="#%E6%9C%9F%E9%96%93">期間</a></h2> <p>2021-01-12 〜 2021-02-21</p> <h2 id="規模"><a href="#%E8%A6%8F%E6%A8%A1">規模</a></h2> <p>第2世代コンパイラのサイズ(行数)はこのくらい(2021-05-25 現在):</p> <pre><code class="sh">$ wc -l lexer.pric parser.pric codegen.pric \ > lib/json.pric lib/std.pric lib/types.pric lib/words.pric 250 lexer.pric 973 parser.pric 1325 codegen.pric 170 lib/json.pric 536 lib/std.pric 466 lib/types.pric 305 lib/words.pric 4025 合計 </code></pre> <pre><code class="sh"># 補助ツール的なもの $ wc -l check.rb preproc.rb 110 check.rb 19 preproc.rb 129 合計 </code></pre> <h1 id="バージョンと世代"><a href="#%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%81%A8%E4%B8%96%E4%BB%A3">バージョンと世代</a></h1> <p>以下、v2/v3 と 第1世代/第2世代 という表現が出てくるので先に説明しておきます。</p> <ul> <li>バージョン: 言語仕様=機能セットの違い <ul> <li>Pric v2: ライフゲームはコンパイルできる。セルフホストはできない。</li> <li>Pric v3: セルフホストに必要な機能を v2 に足したもの</li> </ul></li> <li>世代: 記述言語の違い <ul> <li>第1世代コンパイラ: Ruby で書いたもの</li> <li>第2世代コンパイラ: Pric で書いたもの</li> </ul></li> </ul> <p>※ 今回作った v3 ではライフゲーム(game of life)専用ではなくなるのと、やっぱり呼びやすい名前がほしくなって <strong>Pric</strong> という名前を付けました。 vm2gol = Pric (プロジェクト名・言語名)という雑な捉え方でだいたい大丈夫です。 vm2gol がコードネームで Pric が正式名みたいな雰囲気。</p> <h1 id="工程"><a href="#%E5%B7%A5%E7%A8%8B">工程</a></h1> <h2 id="ステップ (1) 第1世代 v2 コンパイラにセルフホストに必要な機能を追加"><a href="#%E3%82%B9%E3%83%86%E3%83%83%E3%83%97+%281%29+%E7%AC%AC1%E4%B8%96%E4%BB%A3+v2+%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9%E3%81%AB%E3%82%BB%E3%83%AB%E3%83%95%E3%83%9B%E3%82%B9%E3%83%88%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%A9%9F%E8%83%BD%E3%82%92%E8%BF%BD%E5%8A%A0">ステップ (1) 第1世代 v2 コンパイラにセルフホストに必要な機能を追加</a></h2> <p>第1世代なのでここは Ruby のコードの修正です。どのような機能を追加したかについては後述。</p> <p><a href="https://crieit.now.sh/upload_images/03b51b5b2f890722399eb89433f2d9c160d068e93d939.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/03b51b5b2f890722399eb89433f2d9c160d068e93d939.png?mw=700" alt="image" /></a></p> <p>できたもの: 第1世代-v3 なコンパイラ</p> <h2 id="ステップ (2) Pric 言語 v3 でコンパイラ v2 を書く"><a href="#%E3%82%B9%E3%83%86%E3%83%83%E3%83%97+%282%29+Pric+%E8%A8%80%E8%AA%9E+v3+%E3%81%A7%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9+v2+%E3%82%92%E6%9B%B8%E3%81%8F">ステップ (2) Pric 言語 v3 でコンパイラ v2 を書く</a></h2> <p>別の言い方でいえば:</p> <ul> <li>コンパイラ v2 を Pric 言語 v3 に移植</li> <li>ステップ (1) で作った第1世代-v3 なコンパイラで第2世代-v2 なコンパイラをコンパイルできるようにする</li> </ul> <p><a href="https://crieit.now.sh/upload_images/f3ebfb601e870cf878e749b1f6f93f8f60d068f7772ca.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f3ebfb601e870cf878e749b1f6f93f8f60d068f7772ca.png?mw=700" alt="image" /></a></p> <p>ここからは Pric 言語でもりもり書いていきます。<br /> 第2世代コンパイラはこの時点では v2 の機能しかサポートしておらず、ライフゲームはコンパイルできますがセルフホストはまだできません。</p> <p>v2 コンパイラの移植作業はこれまで何度もやってきていてだいぶ慣れています。Pric 版に一番近い <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/09/06/043607">C 言語版</a>もすでにありますし、実装で悩んだら C言語版を見ながら移植していけばOK。また、これまでの移植の過程で整備されてきたテスト用データもそのまま使えます。</p> <p>できたもの: 第2世代-v2 なコンパイラ</p> <h2 id="ステップ (3) 第2世代コンパイラ v2 を v3 にしていく"><a href="#%E3%82%B9%E3%83%86%E3%83%83%E3%83%97+%283%29+%E7%AC%AC2%E4%B8%96%E4%BB%A3%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9+v2+%E3%82%92+v3+%E3%81%AB%E3%81%97%E3%81%A6%E3%81%84%E3%81%8F">ステップ (3) 第2世代コンパイラ v2 を v3 にしていく</a></h2> <p>やることはだいたいステップ (1) と同じ。</p> <p><a href="https://crieit.now.sh/upload_images/698c6d4ba14bb131dd435760e3b15f4860d06904de855.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/698c6d4ba14bb131dd435760e3b15f4860d06904de855.png?mw=700" alt="image" /></a></p> <p>できたもの: 第2世代-v3 なコンパイラ … これができれば完成</p> <hr /> <p>ステップ (2) の移植作業はこれまでやってきた移植作業と同じようにやればいい。</p> <p>ステップ (3) は Ruby で (1) をやったことを繰り返して Pric で同じように書けばいい。</p> <p>というわけで、全体の中ではステップ (1) が今回の目玉でした。今回自分にとって一番収穫の大きかったのがステップ (1)。</p> <p>(1) ができた時点で (3) で何をやればいいか大体の目星が付き、後はやっていけば終わるなという感じで割と気楽に進めていきました。とはいえ、退屈な作業ということはなく、自作の言語でこれくらいの分量のプログラムを書くのは初めての経験でしたから、 (2) も (3) も十分新鮮でおもしろかったです。</p> <h1 id="方針"><a href="#%E6%96%B9%E9%87%9D">方針</a></h1> <ul> <li>セルフホストに必要かどうかで機能を選ぶ <ul> <li>v1 では「ライフゲームのコンパイルに必要か?」で決めていたが、<br /> 今回も同様に「セルフホストに必要か?」で判断する。</li> <li>なるべく寄り道しない / まずは曳光弾を通す / 後回しにできるものは後回し <ul> <li>ここらへんも v1 のときと同じ。目標を絞って集中し、長期化させないように。</li> <li>「とにかくセルフホストできれば細かいところはどうでもいい」くらいの気持ちで</li> <li>セルフホストすることが目的で機能追加はそのための手段</li> </ul></li> <li>後回しにした機能については後述の「TODO」の節を参照</li> </ul></li> <li>なるべく少ない機能で <ul> <li>ミニマル・シンプルに作りたい気持ち</li> <li>禁欲的に / ケチケチと</li> <li>(1) で機能を盛りすぎると (3) が大変になりそう <ul> <li>ステップ (1) で機能を追加するとき、つねに「これを追加するとステップ(3) で大変にならないか?」と考える。ステップ (3) の負担がなるべく小さくなるように意識する。</li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/DQNEO/items/2efaec18772a1ae3c198">DQNEO さんの minigo のセルフホストの話</a>などで大変そうな話を見聞きしていたのが影響していると思います。先達の経験談、ありがたや……。</li> </ul></li> </ul></li> <li>適宜 C言語を参考にするが、あくまで参考程度。C への準拠にはこだわらない。</li> <li>自作言語でセルフホストするのは今回初めてなので、無理しない。適宜ハードルを下げる。 <ul> <li>空間効率、速度効率は気にせず富豪的に。開発効率的に辛くなってきたらそのときなんとかする。先回りしすぎないように。</li> <li>なるべく凝ったことをせず、多少冗長な書き方だったり遅い書き方でもよいので確実に動きそうな方に倒す(そこらへんの改善はセルフホストできた後でやればよい)</li> </ul></li> </ul> <h1 id="セルフホストに必要な機能"><a href="#%E3%82%BB%E3%83%AB%E3%83%95%E3%83%9B%E3%82%B9%E3%83%88%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%A9%9F%E8%83%BD">セルフホストに必要な機能</a></h1> <p>はっきり決めていたわけではありませんが、基本的には C言語版に近い感じになるのかなとイメージしていました。<br /> そうすると、 C言語版では使っていて Pric v2 にない機能をリストアップしていけば、それが v3 で追加すべき機能の候補ということになります。</p> <ul> <li>入出力</li> <li>ポインタ</li> <li>ヒープ / malloc</li> <li>配列</li> <li>文字列</li> <li>構造体</li> <li>グローバル変数</li> <li>プリプロセッサ <ul> <li>ファイル分割 / <code>#include</code></li> <li>マクロ / <code>#define</code></li> </ul></li> </ul> <p>ざっとこんなところかなあとぼんやり考えて、後は作りながら決めていきました。<br /> 以下、思い出しながらそれぞれについて書いてみました。</p> <p>※ 以下は説明のために整理していて、実際に考えたり作った順番とは多少違いがあります。</p> <p>※ ちなみに、私はC言語はずっと前にちょっとやった程度でだいぶ忘れています。素人が書いていると思って読んでいただきたく……勘違い・浅い理解・不正確な記述などあると思います。</p> <h2 id="入出力"><a href="#%E5%85%A5%E5%87%BA%E5%8A%9B">入出力</a></h2> <p>1文字ずつ読み書き。これは必要。<br /> VM に命令を追加: <code>read</code>, <code>write</code></p> <p>入り口と出口で文字と数(アスキーコード)との変換を行い、コンパイル処理本体で扱っているのはすべて単なる数(整数)となるイメージです。</p> <pre><code>"def main()\n ..." ↓ read 100 101 102 32 109 97 105 110 40 41 10 ... ↓コンパイル 32 32 99 97 108 108 32 109 97 105 110 ... ↓ write " call main\n exit\n ..." (アセンブリコード) </code></pre> <p>こうやって見てみると、コンパイラがやっていることは「ある数の並びを別の数の並びに変換する」処理だと見ることができます。おもしろいですね。結局それしかやっていないというか。</p> <p>それはさておき、出力ができるようになると print デバッグができるようになります。また、出力を検証する形でテストできるようになり、これだけでもだいぶ進歩を感じるものです。「おっ、この機能ができたらアレとアレができるようになるじゃん!」という、ブートストラップの楽しさ。</p> <p>hello world はこんな感じ。こんなのでも動くと嬉しい。</p> <pre><code class="ruby">def putchar(c) write(c, 1); end def main() putchar( 72); # H putchar(101); # e putchar(108); # l putchar(108); # l putchar(111); # o putchar( 44); # , putchar( 32); # (SPACE) putchar( 87); # W putchar(111); # o putchar(114); # r putchar(108); # l putchar(100); # d putchar( 33); # ! putchar( 10); # (LF) end </code></pre> <h2 id="比較演算子"><a href="#%E6%AF%94%E8%BC%83%E6%BC%94%E7%AE%97%E5%AD%90">比較演算子</a></h2> <p>演算子としては小なり <code><</code> だけ追加しました。これだけ追加すれば、 v2 からある <code>==</code>、<code>!=</code> と組み合わせてあれこれできます。</p> <p>VM にレジスタを追加: <code>sf</code> (sign flag)<br /> VM に命令を追加: <code>jump_g</code> ... x86 の <code>jg</code> 命令を参考に</p> <h2 id="ファイル分割"><a href="#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%88%86%E5%89%B2">ファイル分割</a></h2> <pre><code>#include lib/std.pric </code></pre> <p>のような行を探し、指定されているファイルを読み込んでその場に埋め込むスクリプトを Ruby で書きました。<br /> が、これは単に cat コマンドで繋げるだけもよかったですね。そうしておけばさらに簡略化できた。</p> <p>※ 機能として挙げましたが、この部分はコンパイラの本体に含めず、補助ツールの類である(セルフホストの対象外)ということにしました。</p> <h2 id="アドレス演算子・デリファレンス演算子"><a href="#%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E6%BC%94%E7%AE%97%E5%AD%90%E3%83%BB%E3%83%87%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9%E6%BC%94%E7%AE%97%E5%AD%90">アドレス演算子・デリファレンス演算子</a></h2> <p>C の <code>&</code> と <code>*</code></p> <ul> <li>ステップ (1) の中ではここがキモ <ul> <li>キモといいつつちょっと怪しいので改めて整理したい……</li> </ul></li> <li>「<a target="_blank" rel="nofollow noopener" href="https://www.sbcr.jp/product/4797337958/">ふつうのコンパイラをつくろう</a>」を読み返す <ul> <li>デリファレンス演算が左辺にあるときと右辺にあるときの違い</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/rui314/chibicc">chibicc</a> や <a target="_blank" rel="nofollow noopener" href="https://github.com/aamine/cbc">cbc</a> の出力するアセンブリを観察 <ul> <li>なるべく最適化されていないアセンブリ出力を見たい</li> <li><a target="_blank" rel="nofollow noopener" href="https://gcc.godbolt.org/">Compiler Explorer</a> もお手軽・便利</li> </ul></li> <li>アドレス演算のために VM に <code>lea</code> 命令を追加 <ul> <li>x86 の <code>lea</code> 命令を参考に</li> </ul></li> <li>「ポインタがある」というか「アドレス演算とデリファレンス演算と、アドレスの入った変数がある」といった雰囲気(一緒?)</li> </ul> <hr /> <p>デリファレンスの括弧の中の部分は単なる式ということにしています。</p> <pre><code>*( some_addr_ + 2 ) ^^^^^^^^^^^^^^ この部分は単なる式 </code></pre> <p>途中紆余曲折があって <code>*(some_addr_, 2)</code> のような書き方を試したりしましたが、括弧の中にはただの式を書くことにして単純化し、C っぽい見た目に落ち着きました。</p> <p>デリファレンス演算の視点(?)では「とにかく何か値(式の評価結果=アドレス)が渡されるので、そのアドレスが指している場所を操作対象とする」のように考えればよい、ということに。</p> <hr /> <p>(余談)乗算の <code>*</code> と被るのでデリファレンス演算子には別の記号を使うのはどうかと考えて <code>^</code> にしてみたり、前置じゃなくて後置にするのはどうだろう? と考えて <code>( ... )^</code> にしてみたり……という試みが途中ありました。</p> <p>下手な考え休むに似たりかと思いきや、Pascal ではそのように書くのだと後から知りました。図らずも似たところに辿りついていたようでちょっとおもしろかったです。</p> <p>他には <code>addr(some_var)</code>, <code>deref(addr_expr)</code> のように普通の関数っぽい構文にしてしまう、という案もありました。パースは少し楽になりそうですが、視認性が悪そう(普通の関数と見分けにくそう)な気がしてボツに。</p> <h2 id="ヒープ / malloc"><a href="#%E3%83%92%E3%83%BC%E3%83%97+%2F+malloc">ヒープ / malloc</a></h2> <p>メモリのスタック領域として使っていた部分のうち、アドレスの小さい方の端をヒープとしました。</p> <p>malloc もまじめに作らないといけないのかなと思ってあれこれ調べていたんですが、単に確保した分だけカーソルを動かすだけなら簡単で私でもすぐ作れそうね、じゃあそれで、ということになりました。</p> <p>※ bump allocator とか stack allocator と呼ばれている方式だそうです。 参考: <a target="_blank" rel="nofollow noopener" href="https://os.phil-opp.com/allocator-designs/">Allocator Designs | Writing an OS in Rust</a></p> <p>というわけで allocate 関数の中身はこれだけ。</p> <pre><code class="ruby">def allocate(g_, size) # 変更前のカーソル位置 var head_addr = *(g_ + GO_ALLOC_CURSOR()); # 変更後のカーソル位置 var next_addr = *(g_ + GO_ALLOC_CURSOR()) + size; # カーソル位置を更新 *(g_ + GO_ALLOC_CURSOR()) = next_addr; # スタックと衝突していないかチェック check_heap_stack_overlap(g_); # 確保した領域の始点アドレスを返す return head_addr; end </code></pre> <p>(<code>g_</code> はグローバル変数構造体のアドレス。後述。)</p> <p>free はありません(C言語版でも free してなかった)。ここは富豪的に。<br /> 富豪的にやってどのくらいメモリが必要だったかというと、最終的には番地の数で 2,600,000 となりました。1 byte/番地で換算すると約 2.6 MB。</p> <p>あと、ヒープとスタックの衝突を Pric コードのレイヤーで検出する作りにしていて、そのためにレジスタ <code>sp</code> の値を取得する組み込み関数を追加しました。ここはちょっとやっつけ。</p> <h2 id="配列"><a href="#%E9%85%8D%E5%88%97">配列</a></h2> <p>配列は作る前からイメージがあって特に詰まらずに作れました。<br /> 「<a target="_blank" rel="nofollow noopener" href="https://www.sigbus.info/compilerbook">低レイヤを知りたい人のためのCコンパイラ作成入門</a>」にならって、普通のローカル変数と同じようにスタックに置きます。</p> <p>構造体の配列を使いたい場合は構造体のデータそのものは持たせず、ポインタというかアドレスを持たせる形で使います。もしデータそのものを持たせるとしたら各要素のオフセットの計算は自前でやらないとダメ(言語によるサポートはない)。</p> <hr /> <p><code>配列変数名[添字]</code> で配列の要素にアクセスする構文を一度用意して、ステップ (1) では実装までしましたが、ステップ (3) でやることが増えるので取りやめにして実装も削除しました。</p> <p>デリファレンス演算子があれば</p> <pre><code class="ruby">a = *(xs_ + 1); *(xs_ + 1) = 2; </code></pre> <p>のように配列の要素を読み書きできます。つまりポインタ演算のシンタックスシュガーなわけですね。<br /> 機能はなるべく少なくしたいという方針に沿って、今回は <code>[]</code> 演算子は取りやめにして実装も削除しました。</p> <p>ただし、「これは配列を扱っているんだぞ」という意図を表すために実際は下記のような関数を介して操作するようにしています。</p> <pre><code class="ruby">def aset(array_, i, val) *(array_ + i) = val; end def aget(array_, i) return *(array_, i); end </code></pre> <p>Pric 言語で書けるものに関してはこのようにユーティリティ関数を作っていきました。</p> <hr /> <p>それで、結局配列のために手を加える必要があったのは</p> <ul> <li>変数宣言時に幅を考慮(スタックポインタを幅1つ分ではなく配列の長さの分ずらす)</li> <li>変数名からスタック上の位置を割り出す際に配列変数の幅を考慮</li> </ul> <p>これだけといえばこれだけですね。<br /> 「配列という機能を追加した」というよりは「変数の幅の考慮とポインタ演算を組み合わせることによって配列の操作に相当する操作ができるようになった」という感覚です。</p> <h2 id="文字列"><a href="#%E6%96%87%E5%AD%97%E5%88%97">文字列</a></h2> <p>配列があればOK。プログラマ(私)が「これは文字列なのだ」と思い込むことにより文字列として扱うことができます。</p> <p>たとえば C だったらこのように書くところを、</p> <pre><code class="c">char str[] = "hello"; </code></pre> <p>Pric では次のように書きます。</p> <pre><code class="ruby">var [6]str; aset(&str, 0, 104); # h aset(&str, 1, 101); # e aset(&str, 2, 108); # l aset(&str, 3, 108); # l aset(&str, 4, 111); # o aset(&str, 5, 0); # (NUL) </code></pre> <p>さすがに毎回手で書くのは面倒なので適当なスクリプトで生成してターミナルからエディタに貼り付けていました。</p> <p>また、コード上での行数が多くて邪魔なのと、複数箇所で同じ文字列を使うケースがあることから、実際は次のように文字列に値を詰める処理を関数にして使っています。</p> <pre><code class="ruby">var [6]s_hello; set_s_hello(&s_hello); </code></pre> <p>ちなみに、文字列関連の何かを調べていたときに Pascal の文字列(先頭に文字列長を持つ)について知りました。Pascal 方式の方がバグが発生しにくそうな感じがしてちょっとなびきかけましたが、ひとまず C に寄せました(Pascal よりは C の方がなじみがあるため)。</p> <h2 id="構造体"><a href="#%E6%A7%8B%E9%80%A0%E4%BD%93">構造体</a></h2> <ul> <li>構造体のサイズ分の領域の確保ができて、</li> <li>起点と、各メンバのオフセットと幅が扱えて、</li> <li>あとはポインタ演算ができれば読み書きできる</li> </ul> <p>心の目で見ると構造体のようなものに見える何か。これもすでにある機能でまかなえてしまいます。</p> <p>構造体のデータはヒープに置いていていますが、これはそういう制約がある訳ではなくとりあえずのルールです。たぶんスタックにも置けるはず。</p> <p><code>some_struct.some_member</code> や <code>some_struct->some_member</code> のような構文は用意せず、操作用のインターフェイスとなる関数を作ってその中でごにょごにょしています。<br /> 例を挙げると以下は List 構造体の size メンバの値を返す getter 関数です。アドレスがあればそれで操作できるぞい、というノリ。</p> <pre><code class="ruby">def List_size(self_) var size = *(self_ + LIST__SIZE()); return size; end # 呼び出し側 # C でいえば list_-&gt;size に相当 var list_size = List_size(list_); </code></pre> <p>結局、メモリ上の位置とデータがあって操作がある、ということでしょうか(それはそう)。</p> <h2 id="グローバルな定数"><a href="#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%81%AA%E5%AE%9A%E6%95%B0">グローバルな定数</a></h2> <p>関数はどこ(他のどの関数)からでも呼び出せますから、固定値を返す関数があればグローバルな定数と等価なものとして扱えます。マクロのような仕組みを追加しなくても、すでにある関数を使えばOK。じゃあそれで。</p> <p>※ ということにしましたが、コンパイラの機能に含めないことにしてプリプロセッサでやってしまってもよかったかも。</p> <p>たとえば 1つ前の「構造体」の節のコード例に出てくる <code>LIST__SIZE()</code> がこれです。</p> <h2 id="グローバル変数"><a href="#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%A4%89%E6%95%B0">グローバル変数</a></h2> <p>たとえば ELF だと .data セクションや .bss セクションに置く、という話があります。<br /> しかし、その方向で進めるとなるとアセンブリ・機械語のファイルのフォーマットやメモリレイアウトも考えないといけなくなります。話が大きくなってハードルが上がってしまう(完成までの道のりが長くなってしまう)ように思われました。</p> <p>そこで、グローバル変数に関してもすでにある機能でなんとかできないかと。</p> <p>要するに</p> <ul> <li>どこでも使えて</li> <li>読み書き両方できる</li> </ul> <p>という要件を満たせれば、その何かをグローバル変数相当のものとして使えます。</p> <p>そこで思いついたのが、エントリポイントである main 関数で構造体を1つ用意して、そのアドレスを関数の引数でバケツリレーしていく方法です。よし、じゃあそれで。</p> <h2 id="機能追加まとめ"><a href="#%E6%A9%9F%E8%83%BD%E8%BF%BD%E5%8A%A0%E3%81%BE%E3%81%A8%E3%82%81">機能追加まとめ</a></h2> <ul> <li>入出力: 追加した</li> <li>比較演算子: <code><</code> だけ追加した</li> <li>ファイル分割: セルフホストの対象外とした</li> <li>アドレス演算子・デリファレンス演算子: 追加した</li> <li>ヒープ / malloc: ほぼ追加なし</li> <li>配列: 幅を考慮して変数を扱えるようにした</li> <li>文字列: 追加なし</li> <li>構造体: 追加なし</li> <li>グローバルな定数: 追加なし</li> <li>グローバル変数: 追加なし</li> </ul> <p>こうして見てみると、今回は</p> <ul> <li>アドレス演算子・デリファレンス演算子を使って間接参照ができるようになった</li> <li>メモリ上のある範囲をまとめて扱えるようになり、間接参照と組み合わせて配列・文字列・構造体相当のデータが扱えるようになった</li> </ul> <p>ことの比重が大きく、これらを利用することでできることの幅がかなり広がった、と振り返ることができそうです。</p> <p>また、グローバルな定数や配列・構造体操作用の関数に加えて <code>-</code>, <code><=</code>, <code>&&</code>, <code>||</code> などの演算子も関数でなんとかしていて、関数があることでなんとかなっている部分も多いですね。関数えらい。抽象化の力。</p> <h1 id="感想など"><a href="#%E6%84%9F%E6%83%B3%E3%81%AA%E3%81%A9">感想など</a></h1> <ul> <li>v1 のときほどのがんばりは必要なく(そういう意味では盛り上がりに若干欠け)比較的落ち着いて進めた感じでしたが、<br /> それはそれでOK。十分楽しく、達成感もありました!</li> <li>先にC言語版を作っていたのが結果として良かった。ここで悩まずに済んだのも大きかった。</li> <li>今回セルフホストまで達成できましたが、思い返してみれば<br /> 一度 v1 を作ったことが足がかりとなり、視界が開けて「自走できるようになった感」が得られたように思います。<br /> がんばって v1 作って良かったなあとしみじみ。</li> <li>ステップ (1) の部分は単純に知識・経験が得られてよかった(低レイヤーの知識がまたちょっと増えた) <ul> <li>メモリ操作用言語でメモリ操作している、みたいな感覚が体験できた</li> </ul></li> <li>(今回は)構造体や文字列など、いろんなものを「見立て」でなんとかしている。つまり、概念は開発者(=自分)の頭の中にある。「これ不便だからこうしたら便利じゃね?」みたいなアイデア(シンタックスシュガーなど)も同様。 <ul> <li>現在使われている言語の機能も、かつて開発者の頭の中にあったものが歴史を経て少しずつコードに落とされてきたものの蓄積……と考えると感慨深いものがありますね。今もいろんな人の頭の中に未来の便利機能のアイデアが眠っているのでしょう。</li> </ul></li> <li>節目で何度か<a target="_blank" rel="nofollow noopener" href="https://prog-lang-sys-ja-slack.github.io/wiki/">言語処理系Slack</a>で進捗報告してリアクションをいただけたのがたいへん励みになりました。ありがとうございました。</li> </ul> <h1 id="TODO"><a href="#TODO">TODO</a></h1> <p>まずは文字列リテラル(に相当する部分)を静的なデータとして使いまわせるようにするところから手を付けたいですね。<br /> おそらくここが速度的に一番のボトルネックになっているので、ここをなんとかするだけでかなり速くなって、後が楽になるんじゃないかと。</p> <hr /> <p>パッと思いつくもの、やりたいなーと思いつつ自重していたのはこのあたり:</p> <ul> <li>文字列リテラル</li> <li>配列アクセス演算子 <code>[]</code></li> <li>演算子の優先順位 <ul> <li>明示的に <code>(</code> <code>)</code> で囲めば済むので結局今回もお流れとなった</li> </ul></li> <li>演算子の追加 <code>-</code>, <code>/</code>, <code>%</code>, <code>></code>, <code>!</code>, <code>&&</code>, etc.</li> <li><code>&&</code>, <code>||</code> の短絡評価</li> <li>まともな構造体</li> <li>まともなグローバル変数</li> <li>型</li> <li>関数の途中での return</li> <li>break/continue</li> <li>その他、雑多な改善: リファクタリング・最適化</li> </ul> <p>あんまり詳しく考えてないですが、C に近づいていくのかなあ。</p> <p>v1, v2 では初心者向け教材っぽくしたくて(想定読者=どうやってコンパイラを作ればいいのか分からなかった頃の自分)「なるべくベタな仕様・見た目にしたい」「高度な機能や独自色をなるべく入れない」という気持ちがあったのですが、v3 ではその縛りは外してしまってもいいかなと思っています。</p> <hr /> <p>というわけで本体は以上で終わりです。以下、苦労話や枝葉な部分のメモです。</p> <h1 id="苦労話"><a href="#%E8%8B%A6%E5%8A%B4%E8%A9%B1">苦労話</a></h1> <h2 id="遅い"><a href="#%E9%81%85%E3%81%84">遅い</a></h2> <p>lexer, parser, codegen を全部コンパイルすると現状では 15分くらいかかります(第2世代コンパイラが育つに連れて伸びていき、最終的にこのくらい)。<br /> ふつうに辛いのですが、終盤ではゴールが近いと分かっていたので下手にジタバタせず逃げ切りました。特に対策せず、実行して待ってる間に部屋の掃除、実行して部屋の掃除、を繰り返していました。部屋がちょっときれいになりました。</p> <p>※ 「TODO」の節で書いたようにこれから速くしていく予定です</p> <h2 id="配列の範囲外アクセス"><a href="#%E9%85%8D%E5%88%97%E3%81%AE%E7%AF%84%E5%9B%B2%E5%A4%96%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9">配列の範囲外アクセス</a></h2> <p>プログラムが大きくなってくるとアセンブリや機械語やメモリのダンプを見てもさっぱり分からん……ので <code>git reset --hard</code> して歩幅を小さくしてやり直して、それで8割くらいはなんとかなった気がします。残りの2割はがんばってなんとか。gdb とか便利なものがないのでここはちょっと辛いですね。</p> <p>ただ、長くても数時間くらいでなんとかなるレベルで、バグの原因が判明するまで何日もかかるということはありませんでした。<br /> なるべく複雑なことをしないように作っていたのが良かったかもしれません。</p> <h2 id="エラー発生時にスタックトレースが見れない"><a href="#%E3%82%A8%E3%83%A9%E3%83%BC%E7%99%BA%E7%94%9F%E6%99%82%E3%81%AB%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E3%81%8C%E8%A6%8B%E3%82%8C%E3%81%AA%E3%81%84">エラー発生時にスタックトレースが見れない</a></h2> <p>print デバッグなどでがんばる……。</p> <p>ある関数で配列の範囲外アクセスが原因と思われるエラーが発生し、おそらく呼び出し元で範囲指定をミスしており、そして呼び出し元がたくさんあり、まず呼び出し元の特定が必要……のようなパターン。</p> <p>セルフホスト達成後に試したところ、VM などにちょいと細工をするだけでスタックトレースを見られるようにできました。若干チート感がありますが、これはさっさとやっておけばよかったかも。</p> <h2 id="関数呼び出し時の引数の個数のまちがい"><a href="#%E9%96%A2%E6%95%B0%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E6%99%82%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%AE%E5%80%8B%E6%95%B0%E3%81%AE%E3%81%BE%E3%81%A1%E3%81%8C%E3%81%84">関数呼び出し時の引数の個数のまちがい</a></h2> <p>これは結構鬱陶しくて、よく間違えるのと、個数が違うのが原因だと分かりにくく不毛だったため、さすがに対策しました。<br /> 構文木の段階で関数の定義と呼び出しを突き合わせてチェックする数十行程度のスクリプトを、これは補助ツールだから……と言い訳しつつ Ruby で書き、パース処理とコード生成処理の間に挟みました。<br /> すごく原始的な静的解析でしょうか。</p> <h1 id="おまけ・細かい話"><a href="#%E3%81%8A%E3%81%BE%E3%81%91%E3%83%BB%E7%B4%B0%E3%81%8B%E3%81%84%E8%A9%B1">おまけ・細かい話</a></h1> <p>v2 から v3 への変更のうち、セルフホストにはそんなに関係ない部分、その他。</p> <h2 id="文頭の set, call, call_set を廃止"><a href="#%E6%96%87%E9%A0%AD%E3%81%AE+set%2C+call%2C+call_set+%E3%82%92%E5%BB%83%E6%AD%A2">文頭の set, call, call_set を廃止</a></h2> <p>v2 では文頭に <code>set</code>, <code>call</code>, <code>call_set</code> を明示的に書く方式にしていましたが、これを不要にしました。ある程度量を書くとなると、やはり鬱陶しいので。これは割と小さめの修正でOK。</p> <pre><code class="diff">-set a = 123; +a = 123; -call foo_func(); +foo_func(); -call_set a = foo_func(); +a = foo_func(); </code></pre> <hr /> <p>関数の引数として任意の式を書けるようにし、また、関数呼び出しを式として扱うようにしました。これにより、たとえば次のように関数呼び出しを関数の引数として書けるようになります。</p> <pre><code class="ruby">foo_func(bar_func()); </code></pre> <p>だいぶ普通の言語っぽくなりました。</p> <h2 id="case 文の else"><a href="#case+%E6%96%87%E3%81%AE+else">case 文の else</a></h2> <p>v2 には else がなく</p> <pre><code class="ruby">case when ... # ... when (0 == 0) # else の代わり # ... end </code></pre> <p>のように書いていたところを、次のように else で書けるようにしました。</p> <pre><code class="ruby">case when ... # ... else # ... end </code></pre> <h2 id="if 文"><a href="#if+%E6%96%87">if 文</a></h2> <p>case文があれば if文はなくていいかなと思っていたのですが</p> <ul> <li>あればあったでちょっと書きやすい</li> <li>少しの手間(数行の変更)で実現できそう</li> </ul> <p>ということで書けるようにしました。</p> <p>レキサで処理しています。<code>if</code> が来たら <code>case</code> <code>when</code> の2つのトークンに変換するだけ。</p> <pre><code class="ruby">if ( ... ) ... end ↓ 変換 case when ( ... ) ... end </code></pre> <p>これだけで <code>else</code> 節のある if文も書けるようになります。</p> <pre><code class="ruby">if ( ... ) # ... else # ... end ↓ 変換 case when ( ... ) # ... else # ... end </code></pre> <h2 id="Ruby っぽい見た目に変更"><a href="#Ruby+%E3%81%A3%E3%81%BD%E3%81%84%E8%A6%8B%E3%81%9F%E7%9B%AE%E3%81%AB%E5%A4%89%E6%9B%B4">Ruby っぽい見た目に変更</a></h2> <p>すでにここまで貼ったコード片で見てきた通りで、 Ruby っぽい見た目に変えてみました。見た目は Ruby っぽいけど中身は C っぽい、ちょっと不思議な書き味です。</p> <p>これはなんとなくできそうだったので、試してみて、なんとなくこうなったというものです。<br /> v2 とのギャップを小さくするために JavaScript っぽい構文に戻そうかなとも考えていましたが、元に戻すのが億劫になるところまで進めてしまって、めんどくさくなって結局そのままという。</p> <p>アドレス演算子、デリファレンス演算子があるため Ruby と文法コンパチにはなっていないと思います。</p> <h1 id="ポインタ変数"><a href="#%E3%83%9D%E3%82%A4%E3%83%B3%E3%82%BF%E5%A4%89%E6%95%B0">ポインタ変数</a></h1> <p>コードの見た目だけでは通常の変数なのかアドレスが入ってる変数なのか、使用箇所を見ても宣言を見ても分かりません。</p> <pre><code class="ruby">var a; # 普通の整数が入る var b; # アドレスが入る </code></pre> <p>そこで、見た目で瞬時に判別できるように、アドレスが入っている変数には末尾に <code>_</code> を付けています。</p> <pre><code class="ruby">def foo( arg1, # 普通の変数 arg2_ # アドレスが入っている ) # ... end foo( a, # 普通の整数を渡している b_ # アドレスを渡している ); </code></pre> <hr /> <p>というわけでおまけコーナーは以上です。また何か思い出したら追記するかもしれません。</p> 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