ハクソク

世界を動かす技術を、日本語で。

スピネル:ルビーAOTネイティブコンパイラ

概要

SpinelはRubyコードをネイティブ実行ファイルにコンパイルするAOTコンパイラ。
全プログラム型推論とCコード最適化でCRubyより高速な実行を実現。
自己ホスティング対応:自身のバックエンドもRubyで記述し自身でコンパイル。
多くのRuby機能をサポートし、独自の最適化や組み込みランタイムを搭載。
依存性最小、高速ビルド、豊富なベンチマーク結果を公開。

Spinelの概要

  • SpinelはRubyソースコードをスタンドアロンなネイティブバイナリに変換するAOTコンパイラ。
  • 全プログラム型推論を用い、最適化されたCコードを生成。
  • CRubyと比較して、計算負荷の高い処理で大幅な高速化を実現。
  • 自己ホスティング:Spinel自身のバックエンドもRubyで記述し、Spinelでコンパイル可能。
  • 依存性最小:生成バイナリはlibcとlibmのみ必要、外部ランタイム不要。

動作フロー

  • Rubyファイル(.rb)をPrismでパースし、ASTをテキスト形式で出力。
  • spinel_codegenで型推論とCコード生成を実施。
  • 標準Cコンパイラ(gccなど)でネイティブバイナリをビルド。
  • 生成されたバイナリは完全スタンドアロン、Ruby環境不要。

クイックスタート

  • make depslibprism取得。
  • makeで全体ビルド。
  • Rubyプログラム(例:hello.rb)を作成。
  • ./spinel hello.rbでバイナリ生成、./helloで即実行。
  • オプションで出力ファイル名指定(-o)、Cコードのみ生成(-c)、Cコード出力(-S)など。

自己ホスティングとブートストラップ

  • Spinelは自身のバックエンド(spinel_codegen.rb)を自己コンパイル
  • ブートストラッププロセス:
    • CRuby+spinel_parse.rbでAST生成。
    • CRuby+spinel_codegen.rbでCコード生成→bin1。
    • bin1でASTからgen2.c生成→bin2。
    • bin2でASTからgen3.c生成し、gen2.cとgen3.cが一致すればブートストラップ完了。

ベンチマーク結果

  • 74の機能テスト、55の性能ベンチマークを完走。
  • miniruby(Ruby 4.1.0dev)比で平均約11.6倍高速
  • 例:
    • life(Conway's GoL):86.7倍
    • ackermann:74.8倍
    • mandelbrot:58.1倍
    • fib(再帰):34.2倍
    • rbtree(赤黒木):22.6倍
    • json_parse:10.1倍
    • ao_render(レイトレーサー):8.0倍

サポートされるRuby機能

  • クラス・継承・モジュール・Struct・alias・定数・オープンクラス
  • 制御構文:if/elsif/else, unless, case/when, while, until, loop, for..in, break, next, return, catch/throw, safe navigation(&.)
  • ブロック・クロージャ:yield, block_given?, &block, proc, lambda, method(:name)
  • 例外処理:begin/rescue/ensure/retry, raise, カスタム例外
  • 主要型:Integer, Float, String(イミュータブル・ミュータブル両対応), Array, Hash, Range, Time, StringIO, File, Regexp, Bigint, Fiber
  • タグ付きユニオンによる多態値、Nullable型(T?)で自己参照構造体対応
  • グローバル変数:静的C変数に変換、型不一致はコンパイル時検出
  • 文字列操作:<<でミュータブル昇格、+や補間、tr、ljust/rjust/centerなど標準メソッド全対応
  • 正規表現:組み込みNFAエンジン、=~, $1-$9, match?, gsub, sub, scan, split
  • Bigint:mruby-bigint利用、必要時のみ静的リンク
  • Fiber:ucontext_tによる協調型コンカレンシー、値受け渡し対応
  • メモリ管理:マーク&スイープGC、サイズ分割フリーリスト、非再帰マーク、スティッキービット
  • 値型最適化:小規模クラスは値型C構造体に昇格しGC不要
  • シンボル:sp_sym型、リテラルはコンパイル時インターン、Hashはシンボル専用実装
  • I/O:puts, print, printf, p, gets, ARGV, ENV[], File.read/write/open, system(), バッククォート

主な最適化内容

  • 型推論駆動の最適化
    • 値型昇格:小規模イミュータブルクラスはC構造体でスタック配置
    • 定数伝播:リテラル定数は直接インライン展開
    • ループ長キャッシュ:配列や文字列長をループ前に計算・再利用
    • メソッドインライン化:短い非再帰メソッドはstatic inline化
    • 文字列連結最適化:複数連結は一括mallocで中間生成抑制
    • Bigint自動昇格:自己参照加算/乗算パターンで自動昇格
    • 静的シンボルインターン:リテラルto_symはコンパイル時定数参照
    • split再利用:ループ内splitは同一配列を再利用しアロケーション削減
    • デッドコード除去:未使用関数はリンク時に自動削除
    • 型推論ループの早期終了:多くのプログラムで1-2回で収束し高速化
    • ASTフィールドパースのバイトウォーク最適化
    • 警告ゼロビルド:全テスト・ベンチマークで-Werrorをクリア

アーキテクチャ構成

  • spinel:ワンコマンドラッパースクリプト(POSIX shell)
  • spinel_parse.c:libprismを直結したCフロントエンド
  • spinel_codegen.rb:AST→Cコード生成(約2万行、自己ホスティング可能)
  • lib/sp_runtime.h:GCや配列・ハッシュ・文字列等のランタイムライブラリ
  • lib/sp_bigint.c:任意精度整数(mruby-bigintベース)
  • lib/regexp/:組み込み正規表現エンジン
  • test/:機能テスト
  • benchmark/:ベンチマーク
  • Makefile:ビルド自動化

ビルドと依存性

  • make deps:libprism取得
  • make:パーサ・ランタイム・ブートストラップコンパイラ構築
  • make test:74機能テスト実行
  • make bench:55ベンチマーク実行
  • make bootstrap:自己ホスティング再構築
  • sudo make install:/usr/localへインストール
  • make clean:ビルド成果物削除
  • Prism:Rubyパーサとして利用、libprism(C)またはprism gem(CRuby)対応
  • CRuby:初回ブートストラップ時のみ必要
  • ランタイム依存性なし:生成バイナリはlibc + libmのみ

制限事項

  • eval・instance_eval・class_eval未対応
  • メタプログラミング(send, method_missing, define_method動的生成)未対応
  • スレッド・Mutex未対応(Fiberはサポート)
  • エンコーディング:UTF-8/ASCII前提
  • ラムダ計算:深いネストのlambda+[]は非対応

ライセンス

  • MIT License採用
  • 詳細はLICENSEファイル参照

Hackerたちの意見

Matzが作ってなかったらかなり疑ってたけど、明確に定義されてるし、彼がRubyのセマンティクスの限界をよく理解してると思う。俺の卒業論文の時(EcmaScript 5が新しかった頃)は、AOT JSコンパイラを作ったんだけど、動いたものの、入力データに関する制限があって、そのせいでやめちゃった。JSの開発者たちは、自分をうまく制限する方法をあまり理解してないみたいだったし(JSON.parseは本質的に未知だし、今はTypeScriptがあるから多分もっと実現可能になった)。制限も明確で、一般的なラムダ計算は型推論システムの限界を指し示してる(Matt Mightみたいな人が書いた良い論文がたくさんある)し、Shed-skin Pythonの人たちも同じ。eval、send、method_missing、define_method、Rubyをあまり知らない俺にとって、これらは実際のコードでどれくらい一般的なの?それに、型なしのパースはどうやって行われるの?(つまり、JSONの取り込みとか?)
> eval、send、method_missing、define_method、Rubyをあまり知らない俺にとって、これらは実際のコードでどれくらい一般的なの? これはコードを書く人によるね。使う人もいれば、あまり使わない人もいる。俺の使い方しか言えないけど、.send()はよく使う。これは特定のメソッドを呼び出すだけだから、理解しやすいと思う。もちろん、.method_name()を使う人もいるけど(Rubyでは通常()なしで)、時々メソッドを自動生成して、動的に何かを呼び出す必要がある時もある。.define_method()は時々使うけど、メソッドを一括で作成する時にね。例えば、HTMLの色名、steelblueやdarkgreenなんかを使って、一括でメソッドを生成することが多い。俺のRubyの主要なプロジェクト50個の中で、せいぜい20個くらいしか使ってないし、約40個は.sendを使ってる(両方ともそれより少ないかも)。eval()は避けるようにしてるけど、いくつかのケースでは使ったり、そのバリエーションを使ったりする。例えば、シンプルだけどバカな電卓では、eval()を使って式を計算する(その前にサニタイズするけど)。理想的ではないけど、シンプルだしね。instance_evalやclass_evalはもっとよく使う、通常はエイリアスのために(俺の脳は悪いからエイリアスが必要で、時々問題を正しく考えるのに役立つ)。method_missingはほとんど使わなくなった。使う場面はあるけど、使うとコードが複雑になって理解しづらくなることが多かったから、ちょっと疲れちゃった。だから、できるだけ避けるようにしてる。避けられないこともあるけど、できるだけ避けたい。で、君の二つ目の質問に答えると、個人的には.send()だけがすごく重要だと思う。他は時々使うけど、そんなに重要じゃない。実際のコードは違うかもしれないけど、Railsのエコシステムは俺には超変わってる。HashWithIndifferentAccessなんて発明したし、理由はわかるけど、理解が足りないことも示してる。これはRailsエコシステムの大きな問題で、Railsの人たちは本当にRubyを知らないか、知らなかったりする。変だよ。「型なしのパース」は、どうしてそれが問題になるのかわからない。たぶん、脳が型に縛られてる人だけがこれを問題だと思うんだろう。型は俺にとって問題じゃない。他の人は反対するかもしれないけど、どこでも問題じゃない。型システムがないと動けない人がいるのは興味深い。通常、Rubyでは振る舞いや能力をチェックするか、怠け者の俺みたいに.is_a?()を使う(これも使うけど、シンプルだから)。実際、俺は.respond_to?()よりも.is_a?()を好むことが多い、打つのが短いからね。使うチェックはシンプルで、「オブジェクト、お前は文字列、ハッシュ、または配列か?」って感じで、これで俺のユースケースの95%はカバーできる。ここで型が必要だとか、どこにフィットするのか全くわからない。追加のセキュリティを提供するかもしれないけど、俺の意見では必要じゃない。
> Matzが作ってなかったらかなり疑ってたけど、明確に定義されてるし、彼がRubyのセマンティクスの限界をよく理解してると思う。 すごく実用的なデザインだね。Prismを使ってるし、Rubyのパースは実際の翻訳よりも難しいくらいだし、Cを生成する。基本的なRubyのセマンティクスはそれほど難しくない。逆に、俺は長い間放置してたバグだらけの純RubyのAOTコンパイラを持ってるけど、自分でホスティングするように書くことにこだわって、自分のパーサーを使うことで、かなり難しくしてしまった。いつかは完成するだろうけど(多分...)。でも、そこから学んだことの一つは、最初の80%を手抜きしても、かなりのRubyコードが動くってこと。残りの「80%」は、Matzがこれ(とMruby)から省いたものに大きく関わってる、エンコーディングやいろんな周辺機能みたいな(Rubyには実際に見たことがない機能がいくつかあるから、いくつかは廃止してほしい)。 > eval、send、method_missing、define_method、Rubyをあまり知らない俺にとって、これらは実際のコードでどれくらい一般的なの?型なしのパースはどうやって行われるの? これらは広く使われてるよ。制限はmrubyのものと似てるけど、用途がある。send、method_missing、define_methodをサポートするのはかなり簡単。eval()をサポートするのはものすごく大変だけど、Rubyでのeval()の使用の大部分は、instance_evalのブロックバージョンに静的に還元できるから、比較的簡単にAOTコンパイルできる。例えば、eval()が呼ばれる文字列を静的に特定できる場合や、分割できる場合、使用の多くは不必要だったり、静的にチェックして処理できる比較的シンプルなイントロスペクションのためのワークアラウンドだから。俺のコンパイラの場合、もしそれがブロッキングの問題になる時が来たら、それが最初のステップになる予定だ。
> eval、send、method_missing、define_method、Rubyをあまり知らない俺にとって、これらは実際のコードでどれくらい一般的なの? かなり多いよ、これがRailsみたいな魔法のようなものを作ることを可能にしてる。100%確信はないけど、おそらく型なしのJSON取り込みの例もそれを使ってる。これを取り除くと、Crystalよりも型が弱いけど、公式のRubyよりもメタプログラミングが少ない、非常にコンパクトで読みやすい言語が残る。だから、かなりの可能性があると思うけど、時間が経てばわかるね。
RubyからObjective Cへのコンパイラがあれば、Rubyの全機能をサポートしつつ、解釈されたRubyよりもパフォーマンスが良くなるかもしれないね。
よくevalを使う方なんだけど、避けられるかもしれないけど、私にはこっちの方が使いやすいんだよね。
これめっちゃクールだね、Ruby用のAOTコンパイラをずっと探してた。evalやメタプログラミングのフォールバックがないのは残念だけど、パフォーマンスの良い小さなサブセットに焦点を当ててるんだろうね。このAOTコンパイラでコンパイルされたgemがMRIとうまく連携できるといいな。もっと標準的なRuby(gemを含む)をパッケージング/バンドルするには、まだtebakoやkompo、ocranが必要だし、ruby-packerやtraveling ruby、jruby warblerみたいな古いプロジェクトも似たようなことをしてた。別の選択肢ができるのはいいけど、もっと開発者に優しい決定的な解決策を期待してる。
ちょっとした背景として、MatzがRubyKaigi 2026で発表したばかり。実験的だけど、Claudeの助けを借りて約1ヶ月で作った。成功したライブデモもあった。彼の新しい猫の名前にちなんで名付けられていて、その猫は『カードキャプターさくら』の猫から取られてるんだ。さくらのパートナーはRubyってキャラなんだよ。
> 実験的だけど、彼はClaudeの助けを借りて約1ヶ月で作った。AIがプログラムをゼロから作ることについてはよく話されるけど、もっとありそうなシナリオを見落としてると思う。AIは10倍のプログラマーを100倍のプログラマーにするかも。Matzの場合は、100倍のプログラマーを500倍にするかもね。
ありがとう!動画はまだライブじゃないみたいだけど、ここで少しずつ公開されてるみたいだよ: https://www.youtube.com/@rubykaigi4884/videos
最近のスピネルのイメージは『スティーブン・ユニバース』のスピネルだから、スピネル/Ruby(ムーン)のダジャレには気づかなかったよ。それがあって、今日はいい日になった。
あ、ルビーっぽい鉱物のことかと思った :)
わお、1ヶ月ちょっとで書かれたんだね。AIについて何を言おうとも、才能あるコーダーの手にかかれば、真剣なスピードアップが可能になるってことだね。
業界の他の人たち: OK、エージェントハーネスを設定して、SOUL.mdを書いて、権限、スキル、mcps、フック、envを設定しなきゃ。 Matz: gem env|infoとfindで十分だよ。
確かにすごいけど、AIエージェントなしではメンテナンスが難しそうだね。spinel_codegen.rbは21,000行もあって、いくつかのメソッドでは15レベルのネストがあるみたい。コンパイラのコードは元々見栄えが良くないけど、それにしても人間がメンテナンスするにはかなり難しいコードだと思う。
spinel_codegen.rbは本当に不気味なものだね。Claudeを使うと、いつもこんなスパゲッティコードになっちゃうんだけど、私が何か間違ってるのかずっと考えてた。今、興味深いアプリケーションが、トップクラスのプログラマーだと思ってる人によって書かれているのを見て、コードのクオリティが一部ではかなりひどいってことに気づいたよ。例えばinfer_comparison_type() [1]。これが最悪の例ではないけど、読みやすくはない。でも、ここで注目すべきは、もっとシンプルで明らかな実装があるのに、Claudeがそれに到達できていないことだね。これをCOMPARISON_TYPES = Set.new(["", "=", "==", "!=", "!"]) def infer_comparison_type(mname) if COMPARISON_TYPES.include?(mname) "bool" else "" end # あるいは、elseケースを省略して # (セットにないものにはnilを返す) end に置き換えたら、もっと短くて、速くて、読みやすくて、メンテナンスもしやすくなるのに、Claudeはいつもif-return、if-return、if-returnのパターンに頼っちゃうんだよね。(even if-elseもClaudeにはちょっと異質な感じ。)私のClaudeコードベースもそのif-returnのゴミでいっぱいだけど、私だけじゃないって分かって安心した。他のファイルはもっと良いコードクオリティだけどね。例えば、libディレクトリのほとんどは、メインラインのRubyリポジトリのextディレクトリに対応してるみたい。APIは明らかにMRI Rubyにインスパイアされてるけど、実装はかなり違うね。MatzがClaudeに元のAPIの一部を反映させるように促したんじゃないかな、これが出力にちょっとした規則性をもたらしたと思う。 [1] https://github.com/matz/spinel/blob/98d1179670e4d6486bbd1547...
制限事項 - evalなし: eval、instance_eval、class_eval - メタプログラミングなし: send、method_missing、define_method(動的) - スレッドなし: Thread、Mutex(Fiberはサポートされてる) - エンコーディングなし: UTF-8/ASCIIを前提 - 一般的なラムダ計算なし: 深くネストされた -> x { } と [] 呼び出し 俺的にはUTF-8/ASCIIを前提にするのは大した制限じゃないけど、他の制限は結構多くのプログラムにとっては問題かもしれない。これらを取り除くにはかなりの作業が必要だと思う。
これでRubyの魔法の大部分が失われちゃうね。
Rubyのスケジューラや基盤のpthread実装はCの環境で問題なく動くはずなのに、「スレッドなし」ってのが気になる。もしかして「ゼロ依存性」を目指してるのかな?それとも後でオプションの「拡張」を計画してるのか、ちょっと変なトレードオフに思える。
特にサポートしないと決めたわけではないみたいだね。たぶん、まだ手が回ってないだけじゃないかな?マルチスレッドは正しく実装するのが難しいからね。
> evalなし: eval、instance_eval、class_eval > メタプログラミングなし: send、method_missing、define_method(動的) > スレッドなし: Thread、Mutex(Fiberはサポートされてる) たくさんのRubyコードを書いてきた者として言わせてもらうと、これが俺が求めるRubyのバージョンだね。シンプルで理解しやすいけど、Rubyの美しさはそのまま。今はLLMの形で非常に生産的なコード生成ツールがあるから、こういう制限のあるRubyの方が実用的だと思う。メタプログラミングは開発者の生産性を上げるためにボイラープレートコードを減らすために存在してるけど、今は開発者がコードを書いてないから、もう必要ないのかも。
Crystalはどう?もし美しさだけが欲しいなら、構文が似てるし静的型付けだから、効率的なコンパイルコードに繋がるかも。
evalがないのは「良いこと」だと思うけど、スレッドやミューテックスがないのはちょっと違うかな。define_methodがないのは使い方を考えると納得できるけど、sendやmethod_missingは既存のライブラリでよく使われてるから、実装するのはそんなに難しくないはず(「コンパイル」(Cに)時のメモリ内ルックアップテーブルを使うなど)。だから、あなたが言う理由で省かれたのか、単にまだ手が回ってないのか。後者だといいけど、互換性のためだけにね。そうじゃないと、短期的には実際の仕事には使えないから。
メタプログラミングの利点は、書くコードが少なくなることじゃなくて、読むコードが少なくなることだよね。
インフラツールに役立ちそうだね。静的にコンパイルされたバンドラーが、RVMとかの役割も果たしつつ(Rubyのインストールとか)でも、まだRubyで書かれているって想像してみて。クラシックなRubyビルドパックはRubyで書かれてるけど、bashでブートストラップしなきゃいけなくて、面倒だし、エッジケースもあるんだよね。CNBはRustで書かれてるから、その問題がないし、依存関係なしで単一のバイナリを出荷できるってアイデアは本当に強力だよ。
アンチたち: 「AIがそんなに役立つなら、AIのシャベルウェアはどこにあるの?AIのオープンソースの貢献は?全部ハイプだよ」 6ヶ月後: マッツがクロードを使ったら、ルビーが1ヶ月の作業で86倍速くなった。もうアンチを真剣に受け止めるのは無理だね。時間が経つことで、すべての主張が覆されてる。歴史は彼らをドットコムのアンチたちと同じように忘れるだろう(つまり、忘れられる)。