Matz の Ruby AOT コンパイラ Spinel を試してみました
RubyKaigi 2026 に参加して、Matz のキーノートで Spinel の発表を聞きました。Spinel は Ruby の AOT コンパイラで、Ruby のコードを読み、C のコードを生成し、最後は native binary として実行できる形にします。Ruby を書いている人間としては、「Ruby の AOT コンパイラ」という言葉だけでテンションが上がります。
Ruby はかなり動的な言語でもあります。メソッド呼び出し、クラスの再オープン、メタプログラミング、eval、実行時に変わるオブジェクトの形。普通に考えると、AOT コンパイルとは相性が悪そうに見えます。
それでも Spinel は、Ruby を Prism で parse し、AST をもとに型推論し、C のコードを生成して native binary にします。発表を聞いていて、これは触ってみたいなと思いました。
この記事では、Spinel の概要、実際に動かしてみた流れ、RubyKaigi 2026 の実スケジュールを使って作った小さな CLI、そして試すときにハマりやすそうなところまで書きます。Spinelは、まだ実験的なプロジェクトです。それでも、Ruby で書いた CLI を単体バイナリとして配れる未来や、AI ツールから呼び出す小さな helper を Ruby で書ける未来を感じました。
Spinel とは何か
Spinel は matz/spinel として公開されている Ruby AOT コンパイラ です。Ruby のソースコードを standalone native executable にコンパイルします。
README に載っている処理の流れは素直です。
Ruby source
-> spinel_parse
-> AST text
-> spinel_codegen
-> C source
-> cc
-> native binary
入口では Prism / libprism を使って Ruby を parse し、AST を text として出します。その AST を spinel_codegen が読み、型推論と C code generation を行います。最後は標準的な C compiler に渡して native binary にします。
面白いのは、spinel_codegen 自体が Ruby で書かれていることです。Spinel は compiler backend を Ruby で書き、その backend 自身を Spinel で native binary にする self-hosting の構成を持っています。Ruby の AOT compiler が Ruby で書かれていて、それ自身をコンパイルする。これだけでもだいぶ熱いです。
README では、74 個の feature test と 55 個の benchmark があり、計算寄りの benchmark では CRuby の miniruby に対して大きな speedup が出ている例も示されています。ただ、ここだけを見て「既存の Ruby アプリがそのまま速くなる」と受け取ると期待値がズレます。
現時点の Spinel は、Ruby の全部をそのまま AOT する魔法の箱というより、Ruby のサブセットを攻めた形で native binary にする実験的なコンパイラとして見るのがよさそうです。Rails アプリや gem-heavy な既存アプリをそのままコンパイルするものではありません。まずは Spinel が理解しやすい Ruby で書いた小さなプログラムや CLI から触るのが現実的です。
なぜ Ruby の AOT がうれしいのか
Ruby で CLI を書くのは楽しいです。文字列処理、ファイル処理、ちょっとした DSL、設定ファイルの変換、コード生成、ログ処理。こういう小さな道具を作るとき、Ruby は今でも気持ちよく書けます。
ただ、配布の段階で急に面倒になります。実行先に Ruby が入っているか。Ruby の version は合っているか。必要な gem は入っているか。Bundler はどうするか。CI や開発者の手元ならまだよいのですが、単体の CLI として配りたいときには、この runtime 依存が重くなります。
そのため、配布しやすい developer tool は Go や Rust で書かれることが多いのが事実です。single binary を作りやすいですし、インストール手順も単純にしやすい。
Spinel が面白いのは、これを変えられそうなところです。Ruby で書いたものを native binary として配れるなら、これまで Go/Rust に逃がしていた小さな CLI の一部を Ruby に戻せるかもしれません。
私が特に期待しているのは、AI coding agent から呼ぶ helper binary です。agent 周辺の helper は、小さく、責務が明確で、stdin/stdout で動けばよいものが多いです。たとえば、設定ファイルを読み替える、ログを要約しやすい JSON に変換する、リポジトリ固有のメタデータを集める、コード生成の前処理をする、などです。
こういうものは Ruby で書くと速いです。人間が読みやすく、変更しやすく、テキスト処理も強い。Spinel で単体バイナリにできるなら、「Ruby で書いた便利な helper を、Ruby runtime なしで agent から呼ぶ」という形が見えてきます。
もちろん、Spinel は既存の Rails アプリの配布問題を一気に解く話ではありません。gem を大量に使ったアプリケーションをそのまま AOT する、という期待はまだ早いと思います。まず想像しやすいのは、私用 CLI、チーム内 CLI、CI helper、agent helper のような、小さく閉じた道具です。
まずは fib で動かす
まずは README の quick start に近いところから触るのが良いでしょう。
Spinel 側の基本手順はこうです。
git clone https://github.com/matz/spinel.git
cd spinel
make deps
make
そのうえで、たとえば次のような fib.rb を用意します。
def fib(n)
if n < 2
n
else
fib(n - 1) + fib(n - 2)
end
end
puts fib(34)
コンパイルは spinel wrapper を使います。
./spinel fib.rb
./fib
-o で出力ファイルを指定できますし、-c で C source だけ出すこともできます。-S を使うと生成 C を stdout に出せます。
./spinel fib.rb -o fib-native
./spinel fib.rb -c
./spinel fib.rb -S
この体験は良いです。Ruby で書いた fib.rb が、最終的に ./fib や ./fib-native のような native binary として動きます。Ruby runtime を起動しているのではなく、C に落ちて、C compiler を通って、普通の executable になります。
ただし、手元では README の手順がそのまま全部通ったわけではありませんでした。make 時の Prism path の解決や、self-host bootstrap まわりは、環境によってハマりやすそうです。このあたりは後ろの「ハマりやすそうなところ」でまとめます。
最小例で感触はつかめたので、次はもう少し RubyKaigi らしい題材として、RubyKaigi 2026 の 3 日分の実スケジュールを使った CLI を作ってみました。
実験: RubyKaigi 2026 セッションプランナーを作る
せっかく RubyKaigi 2026 で Spinel を聞いてきたので、題材も RubyKaigi 2026 にしました。
作ったのは、RubyKaigi 2026 の 3 日分の公式 schedule から、参加プランの一例を出す小さな CLI です。day1, day2, day3 を渡すとその日のプランを出し、引数なしなら 3 日分をまとめて出します。
./build/kaigi-route day1
./build/kaigi-route day2
./build/kaigi-route day3
./build/kaigi-route
データは公式 schedule の day1/day2/day3 を見て、静的な Ruby オブジェクト配列として埋め込みました。
- https://rubykaigi.org/2026/schedule/day1/
- https://rubykaigi.org/2026/schedule/day2/
- https://rubykaigi.org/2026/schedule/day3/
会場は Large Hall / Sub Arena / Small Hall の 3 トラックです。各時間帯には複数のセッションがあるので、その中から 1 つを選びます。
ここでは一例として、runtime / VM / parser / GC / JIT / performance 寄りの基準を置いています。これらは私自身の参加基準そのものではなく、「こういう重みを置くと、低レイヤー寄りの RubyKaigi 2026 巡回プランを作れる」というサンプルです。
Score は公式データではありません。このツール用に付けたローカルな重みです。RubyKaigi のセッションの価値を点数化したものではなく、「この CLI ではこういう関心で選びます」という設定値でしかありません。
実装の中心は、だいたいこんな形になりました。
class KaigiSession
attr_accessor :day, :slot, :start_time, :end_time, :room,
:language, :title, :speaker, :score
def initialize(day, slot, start_time, end_time, room, language, title, speaker, score)
@day = day
@slot = slot
@start_time = start_time
@end_time = end_time
@room = room
@language = language
@title = title
@speaker = speaker
@score = score
end
def line
@start_time + "-" + @end_time + " " + @room + " [" + @language + "] " + @title + " / " + @speaker
end
end
planner は、指定された day と slot に一致するセッションを走査し、もっとも score が高いものを選びます。Hash や複雑なデータ構造は避けて、Spinel が追いやすいように配列と素直な class に寄せました。
class KaigiSchedulePlanner
def initialize(sessions)
@sessions = sessions
end
def best_plan(day_filter)
# day_filter が 0 なら全日程、1/2/3 なら該当日だけを見ます
# 各 day + slot について、一番 score が高い session を選びます
end
end
実際の day3 の出力はこうなります。
RubyKaigi 2026 schedule plan
Focus: Ruby runtime, VM, parser, GC, JIT, and performance
Day 3 - April 24
09:20-10:30 Large Hall [EN/JA] Ruby Committers and the World / CRuby Committers @rubylangorg
10:50-11:20 Sub Arena [EN] Autoresearching Ruby Performance with LLMs / Nate Berkopec @nateberkopec
11:30-12:00 Large Hall [EN] A Write Barrier Validating GC for Ruby / John Hawthorn @jhawthorn
13:30-14:00 Large Hall [JA] (Re)make Regexp in Ruby: Democratizing internals for the JIT / Hiroya Fujinami @makenowjust
14:10-14:40 Large Hall [JA] Ruby Releases Ruby / Hiroshi SHIBATA @hsbt
14:50-15:20 Large Hall [EN] Making Hash Parallel, Thread-Safe and Fast! / Benoit Daloze @eregontp
16:00-16:30 Large Hall [EN] Lightning-Fast Method Calls with Ruby 4.1 ZJIT / Takashi Kokubun @k0kubun
16:40-17:40 Large Hall [JA] Matz Keynote / Yukihiro "Matz" Matsumoto @yukihiro_matz
Sessions: 8
Score: 765
この CLI を CRuby で実行し、Spinel で native binary にしたものと出力が一致することを e2e test で確認しました。
ruby_stdout, = Open3.capture3(RUBY, "src/kaigi_route.rb", "day1")
native_stdout, = Open3.capture3(binary, "day1")
assert_equal ruby_stdout, native_stdout
RubyKaigi の実データを Ruby のコードに埋め込み、それを native binary に焼き込んで ./kaigi-route day3 のように実行する。小さい実験ではありますが、体験としてはかなり楽しいです。
Spinel に寄せて Ruby を書く
Spinel は Ruby らしい見た目で書けます。ただし、普段の Ruby と同じノリで何でも書けるわけではありません。
Ruby は動的に書こうと思えばいくらでも動的に書けます。Hash にいろいろな形の値を入れる、nil と object を混ぜる、メタプログラミングで method を生やす、send で呼ぶ、実行時に class を組み替える。そういう Ruby の柔らかさは、AOT compiler から見ると難しいです。
Spinel で書くときは、「Ruby を書く」というより「Spinel が型推論しやすい Ruby を書く」という感覚があります。
今回の実験では、できるだけ素直な class と array に寄せました。セッションは KaigiSession class にし、全セッションを Array に入れています。Hash もサポートされていますが、まずは型推論を安定させたかったので、key-value で柔らかく持つより、field が決まった object として持つことにしました。
空配列の扱いにも気を使いました。Ruby では items = [] としておいて、あとから何を push しても自然に動きます。しかし AOT compiler では、その配列が何の配列なのかを推論する必要があります。最初に integer を push した配列に、あとから custom object を push するような書き方は避けたほうがよさそうです。
戻り値も同じです。最初は best_session という method を作り、該当するセッションがあれば KaigiSession を返し、なければ nil を返す形にしていました。Ruby としては自然ですが、手元の Spinel ではこの nil | KaigiSession のような戻り値が生成 C で崩れました。
そこで、best_session が object を返す形をやめました。代わりに、planner の中で best_index を持ちます。見つからなければ -1、見つかれば @sessions の index を保持します。最後に @sessions[best_index] で取り出す形です。Ruby としては少し C っぽいですが、型推論は追いやすくなります。
module/class method 経由で custom object を返す形も避けました。最初は KaigiRouteApp.default_planner のような形にしていましたが、手元では生成 C 側で planner が 0 扱いになり、そのまま segmentation fault しました。最終的には default_kaigi_schedule_planner のようなトップレベル関数にし、呼び出し側でも明示的な一時変数に代入してから使う形にしました。
planner = default_kaigi_schedule_planner
plan = planner.best_plan(day_filter)
print_kaigi_plan(plan)
このあたりは、普通の Ruby を書くときの気持ちとは少し違います。とはいえ、制約が分かってくると、小さな CLI なら十分に形になります。むしろ「このコードは compiler にどう見えているか」を意識しながら Ruby を書くのが面白いです。
ハマりやすそうなところ
ここからは、今回 AI と一緒に試しながら見えてきた、ハマりやすそうなポイントを書きます。実際には AI に調べさせたり、生成 C を見てもらったりしながら進めたので、私自身が長時間詰まったというより、「ここは普通に手で進めると詰まりそうだな」と感じたところです。
最初のポイントは make です。手元ではそのまま make すると、Prism の static library を作るところで失敗しました。
ar rcs build/libprism.a
ar: no archive members specified
make: *** [build/libprism.a] Error 1
原因は、Prism gem の path 推定が外れていたことでした。Makefile は ruby -rprism を使って Prism の場所を推定しますが、私の Ruby 4.0.0 環境では Ruby 本体側の path を拾ってしまい、src/*.c が空になっていました。その結果、archive に入れる object が 1 つもありません。
これは PRISM_DIR を明示して回避しました。私の環境では Ruby 3.4.8 に入っていた prism-1.9.0 gem の path を指定しました。
PATH=/path/to/ruby-3.4.8/bin:$PATH \
make PRISM_DIR=/path/to/ruby-3.4.8/lib/ruby/gems/3.4.0/gems/prism-1.9.0
次に、full self-host bootstrap が失敗しました。gen1.c までは進みますが、gen2.c の compile で落ちます。
build/gen2.c:18131:16: error: expected identifier
if (( && (strcmp(...)))) {
^
if (( && ... のような壊れた C が出ていました。私の環境・Ruby/Prism の組み合わせも関係していそうですが、ここでは深追いしていません。
幸い、spinel wrapper には fallback があります。spinel_codegen binary が存在しない場合、Ruby 版の spinel_codegen.rb を使って codegen します。parser と runtime library ができていれば、私の Ruby プログラムをコンパイルする経路はまだ残っています。
最終的には、parser/runtime をビルドし、Ruby codegen fallback 経由で CLI を native binary にできました。
cd vendor/spinel
PATH=/path/to/ruby-3.4.8/bin:$PATH \
make PRISM_DIR=/path/to/ruby-3.4.8/lib/ruby/gems/3.4.0/gems/prism-1.9.0 parse regexp
cd ../..
PATH=/path/to/ruby-3.4.8/bin:$PATH \
vendor/spinel/spinel src/kaigi_route.rb -o build/kaigi-route
もう 1 つハマりやすそうだったのは、compile は通っても実行時に落ちるパターンです。
status=139
segmentation fault です。AddressSanitizer を付けて見ると、sp_KaigiRoutePlanner_best_route 付近で落ちていました。
AddressSanitizer: SEGV
#0 sp_KaigiRoutePlanner_best_route
#1 sp_KaigiRouteApp_cls_run_budget
#2 main
生成 C を見ると、custom object が 0 として扱われていました。最初の実装では KaigiRouteApp.default_planner のような module/class method 経由で planner object を返していましたが、生成された C では planner が 0 になっていました。
lv_route = sp_KaigiRoutePlanner_best_route((sp_KaigiRoutePlanner *)0, lv_budget);
これは当然落ちます。
そこで、module/class method 経由で custom object を返す形をやめました。トップレベル関数で planner を作り、呼び出し側では明示的な変数に受けてから使うようにしました。
planner = default_kaigi_schedule_planner
plan = planner.best_plan(day_filter)
print_kaigi_plan(plan)
もう 1 つ、nil | KaigiSession を返す method も生成 C で型が崩れました。Ruby としては自然にこう書きたくなります。
def best_session(day, slot)
best = nil
# 見つかったら KaigiSession を入れます
best
end
しかし、私の手元ではこの形が mrb_int と sp_KaigiSession * の不一致として C compile error になりました。そこで object を返すのをやめ、best_index = -1 のように integer index で持つ形にしました。
best_index = -1
best_score = 0
# 見つかったら best_index に @sessions の index を入れます
if best_index != -1
session = @sessions[best_index]
end
空配列にも注意しました。Ruby では items = [] として、あとから object を入れていくのは普通です。しかし Spinel の型推論を考えると、その配列が何の配列かを分かりやすくしておきたい。今回の実装では、配列の中身の型が途中で変わるような書き方は避けました。
結果として、CRuby 実行と Spinel 生成バイナリの出力一致まで確認できました。
1 runs, 5 assertions, 0 failures, 0 errors, 0 skips
ここから分かるのは、「Spinel は壊れるからだめ」ということではありません。今の段階では、Spinel が見やすい Ruby に寄せる必要があります。Ruby の柔らかさを全部使うのではなく、AOT compiler が追いやすい形で書く。そうすると、小さな CLI ならちゃんと native binary になります。
低レイヤー的に面白いところ
Spinel の低レイヤー的な面白さは、構成が見えるところにあります。
入口は Prism / libprism です。Ruby source を直接実行するのではなく、まず spinel_parse が Ruby を parse し、AST text に serialize します。
Ruby source
-> Prism / libprism
-> AST text
次に spinel_codegen がその AST text を読みます。ここで whole-program type inference を行い、C source を生成します。
AST text
-> type inference
-> C source
そして最後は、生成された C source を普通の C compiler で compile します。
C source
-> cc
-> native binary
この「Ruby を AST にして、型を推論して、C を吐いて、cc に渡す」という流れが分かりやすいです。もちろん中身は簡単ではありませんが、パイプラインとしては素直です。
もう 1 つ面白いのは runtime です。生成された C がそのまま OS の機能だけで Ruby の object を扱えるわけではありません。Array, String, Hash, GC など、Ruby プログラムを動かすための runtime support が必要になります。
Spinel は sp_runtime.h を持ち、Array/String/Hash などの基本的な runtime を支えます。regexp engine や bigint runtime も持っています。Ruby のコードを native binary にするということは、単に C を吐くだけではなく、Ruby 的な値やメモリ管理を支える小さな実行環境も一緒に持つということです。
いま向いていそうな用途
現時点で想像しやすい用途は、小さく閉じた CLI かなと思います。
たとえば、チーム内 developer tool。リポジトリ固有の設定を読む、特定のディレクトリ構造を検査する、コード生成の前処理をする、CI で使うメタデータを出す。こういうものは Ruby で書くと速いですし、読みやすいです。
CI helper も相性がよさそうです。CI 上で Ruby runtime や bundle install を待たずに、単体バイナリを置いて実行できるなら便利です。もちろん、その helper 自体が gem に強く依存していないことが前提になります。
AI coding agent から呼ぶ helper binary にも期待しています。agent に自然言語で全部やらせるのではなく、リポジトリ固有の確定的な処理は小さな tool に切り出す。その tool を Ruby で書き、Spinel で binary にする。agent はそれを呼ぶだけにする。これは使い道がありそうです。
具体的には、設定変換、コード生成、ログやテキストの整形、特定の JSON/YAML をプロジェクト用の形に直す処理などが考えられます。Ruby はこのあたりが得意です。書きやすく、あとから変えやすい。
計算寄りの小さな処理にも向いていそうです。README の benchmark でも、計算-heavy な処理では大きな speedup が示されています。もちろん benchmark は benchmark として見る必要がありますが、Ruby で書いた小さな計算処理を native binary にするという方向は想像しやすいです。
条件としては、gem 依存が少なく、self-contained で、Spinel のサブセットに寄せて書けること。既存の巨大なアプリをそのまま持ってくるより、新しく Spinel 前提で小さく書く用途のほうが現実的です。
夢を広げるなら、将来的には Ruby 製 CLI の配布にも効いてくるかもしれません。たとえば RuboCop のような Ruby 製 developer tool を single binary として配れたら、インパクトは大きいです。ただ、これは今すぐできるという話ではなく、現時点では「そういう未来を想像したくなる」という温度感です。
今すぐ試すなら、「Ruby で書くと変更しやすい。でも配布は binary にしたい」という小さな道具が一番よいと思います。
まだ割り切りが必要なところ
もちろん、現時点では割り切りも必要です。
まず、今すぐ既存の Rails アプリをそのまま native binary にするものではありません。gem-heavy なアプリも、期待値は下げておいたほうがいいです。Spinel は Ruby のすべてをそのまま持っていくものではなく、Spinel が扱える Ruby のサブセットを AOT するものとして見たほうがよさそうです。
README の limitations でも、いくつかの制約が明記されています。
-
eval,instance_eval,class_evalはサポート外 -
send,method_missing, 動的define_methodのようなメタプログラミングは厳しい -
Thread,Mutexはサポート外 - Fiber は対応している
- encoding 周りは UTF-8/ASCII 前提
Ruby の柔らかさを全部使うより、Spinel が追いやすい Ruby を書く必要があります。データ構造を単純にする。戻り値の型を分かりやすくする。動的に method を生やすより、静的に定義する。Hash に何でも入れるより、class と field で形を決める。
build まわりもまだ実験的です。私の手元では Ruby/Prism の組み合わせや path 推定で転びましたし、full self-host bootstrap も通りませんでした。環境によっては README どおりに進まないこともあります。
ただ、それは「だめ」という話ではありません。今の Spinel は、制約内でどこまで Ruby を native binary にできるかを見る段階のものです。使う側も、コンパイラに少し歩み寄る必要があります。
その歩み寄りを含めて楽しいです。Ruby のコードを書きながら、同時に「このコードは型推論にどう見えているか」「C にはどう落ちるか」を考える。普段の Ruby とは違う筋肉を使う感じがあります。
Matz が AI と短期間でここまで実装したという衝撃
最後に、発表で一番驚いた話に触れたいです。
RubyKaigi 2026 の発表では、Spinel の構想自体は 3 年ほど前からあったという話がありました。そのうえで、今回ここまで実装が進んだ背景として、Matz が AI を活用して短期間で作ったという話がありました。私が現地で聞いた範囲では、数週間でここまで来ており、その間に 3 回作り直した、という話が出ていました。これを聞いたときの衝撃は大きかったです。
ポイントは、構想そのものよりも、以前からあったアイデアをここまで短期間で実装できたというところだと思います。しかも題材は、Ruby の AOT コンパイラです。
もちろん、「AI が全部作った」という単純な話ではありません。むしろ大事なのは、Matz のような強い言語設計者が AI を使うと、低レイヤーの実験でも探索速度が上がる、という点です。
コンパイラを書くには、言語仕様、parser、型、runtime、生成コード、テスト、benchmark、全部が絡みます。普通に考えると、試行錯誤のコストが高いです。ところが AI を使うことで、その試行錯誤の回転数を上げられるのかもしれません。
「3 回作り直した」という話は、単に迷走したというより、設計探索を高速に回した話として面白いです。コンパイラのような低レイヤーのソフトウェアでも、AI を使って仮説を立て、実装し、捨て、また作る、というサイクルを短期間で回せる。
ここで大事なのは、AI 単体の能力を過大評価することではありません。強い人が AI を使うと、探索できる範囲が広がる。Spinel は、その分かりやすい例に見えました。
Spinel 自体も追いかけたいですし、AI-assisted development の実例としても追いかけたいです。数週間でここまで来たなら、数か月後にどうなっているのか期待してしまいます。
まとめ
RubyKaigi 2026 で Spinel の発表を聞いて、かなり驚愕しました。実際に触ってみても、Ruby で書いたものが native binary になる体験はやはり楽しいです。
もちろん、現時点の Spinel は実験的です。普通の Ruby をそのまま何でもコンパイルできるわけではありません。Spinel が型推論しやすい Ruby に寄せる必要がありますし、build や codegen でハマりやすそうなところもあります。
それでも、その荒さを含めて面白い段階です。Ruby のコードが C になり、runtime support と一緒に compile され、Ruby runtime なしで実行できる。その流れを手元で見られるだけでも価値があります。
特に期待しているのは、CLI 配布と AI tool helper です。Ruby で書くと変更しやすい小さな道具を、単体バイナリとして配る。AI coding agent から呼ぶ確定的な helper を Ruby で書く。そういう用途なら、Spinel の方向性はかなり有効だと思います。
興味が出た人は、まず matz/spinel を clone して、fib のような小さな Ruby を試してから、次に、個人用の小さな CLI を 1 つ Spinel に寄せて書いてみるのが良さそうです。
Discussion