Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
S

spinel

> 编程语言
Open source

Spinel -- Ruby AOT Compiler Spinel compiles Ruby source code into standalone native executables. It performs whole-program type inference a…

1.7K stars0 likes1 views
WebsiteGitHub

About

Spinel -- Ruby AOT Compiler Spinel compiles Ruby source code into standalone native executables. It performs whole-program type inference a…

Spinel -- Ruby AOT Compiler

Spinel compiles Ruby source code into standalone native executables. It performs whole-program type inference and generates optimized C code, achieving significant speedups over CRuby.

The compiler is a single self-contained C binary: it parses Ruby (via libprism), infers types across the whole program, emits C, and invokes the system cc to produce a native executable -- no Ruby runtime and no chained helper binaries at compile time. (Spinel was previously written in a self-hosting Ruby subset; that backend is preserved on the self-host branch -- see History.)

How It Works

…

Analyze and codegen run in one process and share the in-memory model: codegen reads the types analyze just inferred directly, with no serialization step in between. See docs/internals/AST.md for the text AST format the parser emits and the analyzer consumes.

Quick Start

Spinel is built from source: clone this repository and build with make. It is not distributed as a RubyGem, and there is no gem install spinel. (Spinel compiles certain gems into your program, but the compiler itself ships as source.)

bash
# Fetch libprism sources (from the prism gem on rubygems.org):
make deps

# Build everything (the compiler `spinel` and the project tool `spin`):
make
sudo make install     # optional: puts spinel and spin on PATH

# Start a project:
spin new myapp && cd myapp
spin run              # compile bin/myapp.rb and run it

Releases are dated tags, YYYY.MM.DD (spinel --version names the one a build belongs to, +N when the build is N commits past it, then the git revision in parentheses); the name says when a release was cut, and what it promises is in docs/limitations.md and the tests. The first one is 2026.09.12. Each release carries a source archive (spinel-<release>.tar.xz, what make dist produces) with the vendored parsers included, so it builds with make alone and no network.

spin is the day-to-day interface — cargo/mix style. It scaffolds projects, resolves dependencies, drives the compiler, and runs tests; no Makefile, no hand-written -I flags:

bash
spin add ansi --version "~> 1.0"   # from the spin package index
spin add mylib --path ../mylib     # or a local checkout / --git URL
spin test                          # snapshot tests (CRuby is the oracle)
spin build && ./build/bin/myapp

Dependencies (packages) are source trees compiled into your binary — no runtime loading, no extension builds; a package can even carry .c files. See docs/spin.md for the full guide, including how to write and publish a library.

Single files: the compiler directly

spinel is the underlying compiler — gcc-like, one job — and stays the right tool for single-file scripts and experiments:

bash
cat > hello.rb <<'RUBY'
def fib(n)
  if n < 2
    n
  else
    fib(n - 1) + fib(n - 2)
  end
end

puts fib(34)
RUBY

./spinel hello.rb
./hello               # prints 5702887 (instantly)
bash
./spinel app.rb              # compiles to ./app
./spinel app.rb -o myapp     # compiles to ./myapp
./spinel app.rb -c           # generates app.c only
./spinel app.rb -S           # prints C to stdout
./spinel -E app.rb a b c     # compile, run with ARGV=[a, b, c], discard binary
./spinel -e 'puts 42'        # compile inline source
./spinel app.rb --int-overflow=wrap   # +/-/* wrap silently instead of raising

./spinel is a single native binary (build/spinel; the repo-root spinel is a convenience symlink make creates) that parses, infers types, emits C, invokes cc to link it, and can run the result — no shell wrapper or chained helper binaries, no network, no manifest knowledge (that separation is what keeps builds hermetic; spin owns the stateful side). It supports the full option set, including --rbs DIR (RBS-seeded inference) and the --emit-rbs / --emit-types / --emit-symbol-map analysis modes.

Integer overflow

Integers are native fixed-width words. --int-overflow=MODE selects how +/-/* behave when a result exceeds that width:

  • raise (default) -- raise on overflow. Safe (never silently wrong or undefined), but 9223372036854775807 + 1 raises instead of returning a Bignum.
  • wrap -- silent two's-complement wrap. Fastest, no check.
  • promote -- escalate to arbitrary-precision Integer (Bigint), matching CRuby. 9223372036854775807 + 1 returns 9223372036854775808.

In the default mode, integer locals that an obvious growth pattern would overflow (e.g. a q = q * k accumulator) are still auto-promoted to Bigint; promote extends that to every integer operation.

RBS type signatures

Spinel can read RBS files to seed the analyzer. When invoked with --rbs DIR, spinel runs spinel_rbs_extract over a directory of *.rbs files (the same layout rbs and Steep use) and feeds the resulting seed into the analyzer. Seeds are advisory — inference still runs on top and widens on observed contradiction, so a wrong or unrepresentable seed is at worst a no-op. See docs/rbs-extract.md for the supported subset.

Benchmarks

3,651 tests pass. 62 benchmarks pass. Geometric mean: ~8.5x faster than Ruby 4.0.4 with --yjit across the 28 benchmarks below (~15.2x against the interpreter). Measured 2026-09-17 on spinel 74c8e0a4 (32-core Linux box, gcc 13.3). Baseline is CRuby 4.0.4 (stable, revision b89eb1bc), run with --disable-gems and with --yjit for the JIT column. Each timing is the mean of five wall-clock runs under perf stat -r 5, which held every cell inside a few percent. A compiled binary starts in about 0.8 ms, so a sub-10 ms Spinel cell is mostly process startup and its ratio should be read as a lower bound on the machine, not as a measurement of the loop.

Computation

Benchmark Spinel Ruby 4.0.4 + YJIT Speedup vs YJIT
mandelbrot 18 ms 939 ms 938 ms 53.6x
fib (recursive) 1.1 ms 387 ms 49 ms 44.6x
matmul 6.1 ms 200 ms 201 ms 32.9x
nqueens 6.0 ms 147 ms 141 ms 23.6x
sieve 14 ms 274 ms 274 ms 19.2x
partial_sums 41 ms 786 ms 777 ms 19.0x
life (Conway's GoL) 19 ms 499 ms 229 ms 12.1x
sudoku 3.3 ms 63 ms 32 ms 9.6x
fannkuch 1.3 ms 9.2 ms 8.9 ms 6.8x
fasta (DNA seq gen) 1.5 ms 8.4 ms 8.8 ms 5.9x
tak 7.8 ms 320 ms 46 ms 5.9x
ackermann 6.7 ms 290 ms 39 ms 5.8x
tarai 6.9 ms 255 ms 38 ms 5.5x

Data Structures & GC

Benchmark Spinel Ruby 4.0.4 + YJIT Speedup vs YJIT
so_lists 12 ms 259 ms 159 ms 13.1x
huffman (encoding) 5.4 ms 38 ms 39 ms 7.2x
linked_list 23 ms 189 ms 153 ms 6.7x
gcbench 196 ms 2_114 ms 952 ms 4.9x
binary_trees 2.7 ms 23 ms 13 ms 4.7x
rbtree (red-black tree) 15 ms 355 ms 67 ms 4.5x
splay tree 8.7 ms 116 ms 39 ms 4.5x

Real-World Programs

Benchmark Spinel Ruby 4.0.4 + YJIT Speedup vs YJIT
ao_render (ray tracer) 70 ms 1_646 ms 611 ms 8.7x
bigint_fib (1000 digits) 0.9 ms 5.7 ms 6.1 ms 6.8x
pidigits (bigint) 1.0 ms 5.8 ms 6.2 ms 6.2x
template engine 56 ms 460 ms 328 ms 5.9x
json_parse 28 ms 212 ms 133 ms 4.7x
str_concat 1.3 ms 6.4 ms 6.0 ms 4.6x
csv_process 95 ms 491 ms 396 ms 4.2x
io_wordcount 14 ms 49 ms 44 ms 3.1x

A few notes on what YJIT does and doesn't change. On some integer-loop workloads (mandelbrot, nqueens, matmul, partial_sums, sieve) YJIT's numbers are essentially identical to interpreted Ruby; the benchmark is bound by integer / float operations that the interpreter already runs at native speed. On call-heavy code (ackermann, tarai, tak, rbtree) YJIT gives a real 5-8x lift over the interpreter, which is why those rows carry Spinel's smaller multiples.

One row deserves a caveat rather than a boast. fib is not 45x faster per call: its whole call tree is a pure function of a literal, so the C compiler collapses most of it at build time -- fib(42) retires 231M instructions where the recursion itself would make 866M calls. Read it as "the C compiler got to see the whole program," which is the point of compiling ahead of time, but not as a per-call number.

The genuinely narrow cells are io_wordcount, bound by line reading and hash updates that neither compiler can specialize, and csv_process and gcbench, bound by allocation.

Supported Ruby Features

Core: Classes, inheritance, super, include (mixin), attr_accessor, Struct.new, alias, module constants, open classes for built-in types.

Control Flow: if/elsif/else, unless, case/when, case/in (pattern matching), while, until, loop, for..in (range and array), break, next, return, catch/throw, &. (safe navigation).

Blocks: yield, block_given?, &block, proc {}, Proc.new, lambda -> x { }, method(:name). Block methods: each, each_with_index, map, select, reject, reduce, sort_by, any?, all?, none?, times, upto, downto.

Exceptions: begin/rescue/ensure/retry, raise, custom exception classes.

Types: Integer, Float, String (immutable + mutable), Symbol, Array, Hash, Range, Time, StringIO, File, Regexp, MatchData, Complex, Rational, Bigint (auto-promoted), Enumerator, Set, Fiber, Thread, Mutex, Queue, SizedQueue, ConditionVariable, Marshal (dump/load). Polymorphic values via tagged unions. Nullable object types (T?) for self-referential data structures (linked lists, trees).

Inspect / p: Object#inspect produces CRuby-byte-identical output across the whole type surface -- primitives, typed and heterogeneous arrays, every Hash variant, Range, Struct, and user-class instances (#<Name:0x... @ivar=...>, or a user-defined inspect if present), including values reached through a polymorphic (tagged-union) binding. p obj, obj.inspect, obj.to_s, and "#{obj}" interpolation all agree.

Global Variables: $name compiled to static C variables with type-mismatch detection at compile time.

Strings: << automatically promotes to mutable strings (sp_String) for O(n) in-place append. +, interpolation, tr, ljust/rjust/center, and all standard methods work on both. Character comparisons like s[i] == "c" are optimized to direct char array access (zero allocation). Chained concatenation (a + b + c + d) collapses to a single malloc via sp_str_concat4 / sp_str_concat_arr -- N-1 fewer allocations. Loop-local str.split(sep) reuses the same sp_StrArray across iterations (csv_process: 4 M allocations eliminated).

Regexp: Built-in NFA regexp engine (no external dependency). =~, $1-$9, match?, gsub(/re/, str), sub(/re/, str), scan(/re/), split(/re/).

Bigint: Arbitrary precision integers via mruby-bigint. Auto-promoted from loop multiplication patterns (e.g. q = q * k), or from every integer operation under --int-overflow=promote (see Integer overflow). Linked as static library -- only included when used.

Fiber: Cooperative concurrency via a small portable assembly context switch (x86-64 / arm64; no ucontext dependency). Fiber.new, Fiber#resume, Fiber.yield with value passing, Fiber#raise/#kill, external Enumerators and Enumerator::Lazy ride the same machinery. Captures free variables via heap-promoted cells. Per-fiber storage via Fiber[:k] / Fiber[:k] = v (and the Fiber.current[:k] aliases) — symbol-keyed poly-valued, lazily allocated, shallow-snapshot inherited from the parent at Fiber.new time.

Threads: Thread runs with true parallelism and no GVL — an M:N scheduler multiplexes green threads onto OS workers (one per c

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

C

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言