A pure-Go implementation of Ruby — one static binary, full dynamism, zero cgo.
This repository is the interpreter: a compiler that lowers Ruby to bytecode, and
a stack VM (mruby/YARV lineage) that runs it. The Ruby front-end (lexer,
parser, AST) is the standalone pure-Go
go-ruby-parser module, which this
interpreter imports. The front-end is embedded in the binary, so eval and
runtime require keep working. Ruby
objects are Go heap objects, so Go's garbage collector is reused. Dispatch
goes through mutable per-class method tables, which is what makes
monkey-patching, define_method and method_missing free.
🌐 Website · 📚 Documentation · 🧭 Roadmap
rbgo is increasingly assembled from a family of standalone pure-Go (CGO=0), MRI-compatible libraries — each its own org with 100% coverage and 6-arch CI — that the interpreter composes and binds as native modules. The design principle is a clean seam: a stdlib piece whose work is pure compute and needs no interpreter — a regexp matcher, an ERB compiler, a YAML emitter, a Marshal codec, an OptionParser argv engine — becomes a reusable standalone library, while only the thin interpreter-dependent glue (the Ruby-object binding) stays in rbgo. The same lever that produced the front-end (go-ruby-parser) is now applied across the stdlib. The benefit cuts both ways: rbgo still ships as a single CGO=0 static binary, and every extracted piece is independently reusable, tested and 6-arch by any Go program — no interpreter required.
The go-ruby-* family is a growing set of standalone pure-Go modules — all
CI-green, 100% coverage, 6-arch — each its own org. The go.mod currently
binds 181 of them into rbgo as native modules:
| Library | Role | Org · landing |
|---|---|---|
| go-ruby-parser | Ruby lexer / parser / AST front-end (embedded, so eval/require keep working) |
org · site |
| go-ruby-regexp | Onigmo-compatible regexp engine (incl. \b/\B, char-class literals) |
org · site |
| go-ruby-erb | ERB template compiler | org · site |
| go-ruby-marshal | Marshal (dump/load), byte-exact with MRI |
org · site |
| go-ruby-yaml | Psych-compatible YAML emitter + loader | org · site |
| go-ruby-format | sprintf / % / format engine |
org · site |
| go-ruby-optparse | OptionParser argv engine |
org · site |
| go-ruby-strscan | StringScanner (strscan) |
org · site |
The full bound family (alphabetical, all native modules in go.mod):
aasm, abbrev, acme, actioncable, actionmailer, actionpack,
actionview, activejob, activemodel, activerecord, activestorage,
activesupport, addressable, age, arrow, async, augeas, base64,
bbolt, bcrypt, benchmark, bigdecimal, bleve, builder, bundler,
cancancan, capistrano, capybara, cgi, chronic, cmath, commonmark,
concurrent-ruby, confd, connection-pool, csv, date, deep-merge,
devise, did-you-mean, digest, dotenv, dry-struct, dry-types,
dry-validation, erb, erubi, etcd, excon, facter, factory-bot,
faker, faraday, fast-gettext, find, format, friendly-id,
getoptlong, grape, graphql, grpc, haml, hanami, hcl2, hiera,
hocon, http, httparty, i18n, images, ipaddr, irb, jbuilder,
jekyll, json, jwt, kafka, kaminari, kramdown, liquid, logger,
mail, marshal, matrix, mime-types, minitest, money, mongodb,
msgpack, multi-json, mustache, mysql, nats, net-ftp, net-http,
net-imap, net-pop, net-sftp, net-smtp, nokogiri, oauth2, observer,
oidc, omniauth, openbao, openstack, opentelemetry, opentype,
optparse, ostruct, pagy, paper-trail, parquet, parser, pathname,
pg, prawn, prettyprint, prime, protobuf, pstore, public-suffix,
puma, pundit, puppet, puppet-resource-api, racc, rack, rails,
railties, rake, ransack, rdoc, redis, regexp, reline, resolv,
resque, rexml, roda, rolify, rouge, rqrcode, rspec, rss,
rubocop, rubygems, saml, sass, scanf, securerandom,
semantic-puppet, sequel, set, shellwords, shrine, sidekiq,
simplecov, sinatra, slim, sodium, sqlite3, strscan, thor,
timecop, toml, tsort, typhoeus, tzinfo, unicode-normalize, uri,
vcr, warden, webauthn, webmock, webrick, widgets, xslt, yaml,
zeitwerk, zlib — each at github.com/go-ruby-<name>/<name>.
Beyond the go-ruby-* family, the scientific / container stack binds the
pure-Go go-ndarray,
go-fft, go-images
and go-composites libraries the same way (see
Supported today below), and the pure-Go go-widgets
UI toolkit is bound through three adapters: require "widgets" (pixel-blitting
GUI toolkit), require "tui" (terminal-cell toolkit) and require "mvvm"
(data-binding layer) — each at github.com/go-ruby-widgets/<name>.
A major conformance + performance campaign measured rbgo's pure-Go front-end (parse + compile) against the MRI 4.0.5 oracle on the two largest real-world Ruby codebases and a suite of popular libraries. All figures are front-end acceptance, not whole-application execution (see the honesty note below).
- Parse: 99.96 % of Rails + Puppet (5577 / 5579
.rbfiles). The only 2 misses are intentional syntax-error fixtures MRI itself rejects — i.e. 100 % of all valid Ruby in the corpus parses. - End-to-end (parse + compile): Rails 99.82 % (3417 / 3423), Puppet 100.00 % (2154 / 2154).
- 0 over-permissive — rbgo never accepts Ruby that MRI rejects.
- Library parse-conformance: RuboCop 99.7 %, Sinatra / Jekyll / Thor / Kramdown / dry-struct 100 %, Homebrew 98.7 %, Chef 99.1 %, concurrent-ruby 98.3 %, Asciidoctor 93.8 %.
- RSpec DSL usage: 10/10 byte-identical to MRI.
- The journey (Rails end-to-end, across 5 parser rounds + 5 rbgo activation rounds): 20.7 % → 46.3 % → 68.7 % → 81.2 % → 86.7 % → 93.8 % → 98.4 % → 99.82 %.
Features added in the campaign include :: constant-paths (qualified class /
module names, superclass, leading ::), paren-less command calls with
args/kwargs, class << self (singleton class), masgn to any target
(ivar/cvar/gvar/attr/index/constant + nested destructuring), alias/undef,
anonymous params + forwarding (def f(*, **, &) / g(...)), special globals
($$ etc.), shorthand hash {x:}, quoted / operator / char-literal symbols,
rationals & imaginaries (2r / 3i), unicode identifiers, for…in…end loops,
block keyword params, begin-less rescue/ensure, and !~ — on top of the
pre-existing core (full object model, metaprogramming, Fiber/Thread, Marshal,
regexp, the scientific bindings, and the js/wasm target) detailed below.
Performance: a 6-runtime comparative suite (BENCHMARKS.md) pits rbgo and rbgo+AOT against MRI, MRI+YJIT, JRuby and TruffleRuby. The rbgo interpreter runs ~3–6× MRI on compute and at parity on I/O-bound work; rbgo+AOT beats MRI+YJIT 18–24× on loop/fib (the only runtime here that beats YJIT); TruffleRuby is the compute ceiling.
Honesty note. "99.82 % of Rails" means rbgo's front-end parses and compiles that fraction of Rails's
.rbfiles — not that rbgo runs Rails. Running a full application additionally needs the runtime stdlib surface and C-extension equivalents. That surface is now real enough that the realpuppet applyCLI runs end-to-end under rbgo (see Running Puppet below), convergingnotify,fileandexecresources; the broader resource providers (package/servicehost convergence) remain in progress. What is established is that the Ruby language / front-end is essentially complete on real-world code, and that the runtime is far enough along to boot a real application; whether any given application runs end-to-end remains application-specific work. Details: CONFORMANCE-RAILS-PUPPET.md, CONFORMANCE-LIBRARIES.md, CONFORMANCE-RSPEC.md.
Beyond parsing real-world Ruby, rbgo now runs the real Puppet
puppet apply CLI end-to-end. require "puppet" fully boots the framework
(Puppet 8.11.0) on a pure-Go CGO=0 rbgo — its pure-Ruby gem dependencies
(semantic_puppet, concurrent-ruby, deep_merge, fast_gettext, facter,
racc, …) load on the $LOAD_PATH — and a manifest then travels the complete
Puppet path: the real Puppet::Util::CommandLine → Puppet::Application::Apply
entry point (a real OptionParser), every Puppet type + provider loaded, the
settings catalog applied (creating Puppet's config dirs on disk), and finally the
user catalog applied through the transaction / RAL. Driving the genuine CLI emits
real Puppet output and exits 0:
$ rbgo run puppet_apply.rb # ARGV = apply -e 'notify { "hello": message => "hi from rbgo cli" }'
Notice: Compiled catalog for in environment production in 0.00 seconds
Notice: hi from rbgo cli
Notice: /Stage[main]/Main/Notify[hello]/message: defined 'message' as 'hi from rbgo cli'
That is the actual notify resource type applying through the transaction and the
Resource Abstraction Layer — not a notice(...) evaluator print. Reaching the CLI
added a real OptionParser (optparse), File::Stat / FileTest + on-disk
filesystem operations, and a batch of deep Ruby fixes (class_eval lexical
scope, return inside define_method, class-method super, String#chomp(sep),
…).
Three resource types now converge end-to-end through the transaction / RAL:
notify, file (the catalog creates / manages a real file on disk), and
exec — which runs its command through pure-Go process execution, honouring
the onlyif / unless / creates / path guards (so an already-converged
exec is correctly skipped). puppet apply exits cleanly, the YAML run
report / state round-tripping through the pure-Go Psych emitter / loader.
Reaching this exercised a large slice of the runtime, each gap reduced to a minimal rbgo-vs-MRI repro before fixing:
- Language / VM conformance:
autoload, frame-basedException#backtrace/set_backtrace/full_message, interpolated regexp literals, non-local blockreturn,Symbol#intern,NilClassconversions,Arrayslice-assignment,Module.new(real anonymous module running its block as a body),extendtransitivity (a module's transitively-included methods become class methods), setter expressions returning their RHS, nested constant namespaces, and method-visibility enforcement — among dozens more. - Pure-Go stdlib modules added on the path to boot and run the CLI:
ERB(template engine),openssl(real crypto, not a stub),net/http,resolv,tmpdir,Process,StringScanner(strscan),Find,getoptlong,syslog,fileutils, a realOptionParser(optparse),File::Stat/FileTest+ on-disk filesystem operations,objspace, and more — eachrequire-able and CGO=0.
This is the C-extension → pure-Go shim strategy in action: a real Ruby application ships as a single static CGO=0 binary because the C-backed gem APIs are backed by pure Go. Puppet validates the approach end to end — its dependency tree is pure Ruby, so it loads as-is.
Frontier (honest): what runs end-to-end is the
puppet applyCLI convergingnotify,fileandexecresources through the full pipeline (boot → parse → compile → transaction / RAL → output → clean exit). The next frontier is the broader resource providers —user/groupare provider-ready, whilepackage/serviceneed a host package manager / systemd and root. So the language, front-end, boot, CLI/apply machinery and the first convergent providers are real today; the remaining system-state providers are the work in progress.
Supported today (every feature differential-tested against MRI Ruby 4.0.5 and JRuby):
- Values: integers (
int64, with automatic Bignum promotion on int64 overflow and arbitrary-precision integer literals, radix literals0x/0o/ bare-0-octal/0b/0dwith underscores), floats, strings, symbols (incl. operator-method symbols:+/:<</:[]=/:<=>, usable withreduce(:+)/inject/send(:+, x)), arrays, hashes, ranges (incl. beginless/endless),ComplexandRationalnumbers,true/false/nil,self,Proc/lambda,Regexp/MatchData,Struct. - Operators: arithmetic (
+ - * / %, Ruby floor division,**), comparison/<=>,==/===, bitwise/shift (<< >> & | ^ ~, arbitrary precision),&&/||, ternary, ranges,::constant scope (Math::PI,Foo::BAR); correct negative-literal precedence (-2.abs == 2,-2**2 == -4). - Control flow:
if/elsif/else,unless,while/until,case/when, statement modifiers (incl. modifierrescue,expr rescue fallback),begin/rescue/else/ensure/retry,break/next,Kernel#loop, andFiber(cooperative coroutines —Fiber.new/resume/Fiber.yield/alive?). - Concurrency:
Thread(new/start/join/value/status/current/main/list/pass, thread-locals via[]/[]=, exception propagation on join),Mutex(lock/unlock/try_lock/synchronize/owned?) andQueue(blockingpush/pop,close), on an emulated GVL — one Ruby thread runs at a time, matching MRI's memory model (race-free under the Go race detector). - Pattern matching (
case/in): value, variable-binding, class/constant, array (incl. splat and nested), hash (deconstruct_keys,**rest/**nil), find ([*pre, x, *post]), pin (^x) and alternative (a | b) patterns; the=> namebinding suffix, guards (if/unless), the one-line formsexpr => patternandexpr in pattern, andNoMatchingPatternError— via thedeconstruct/deconstruct_keysprotocols. - Assignment: multiple assignment / destructuring (
a, b = 1, 2, swap,x, *rest = …,*init, last = …), compound assignment (+= -= *= /= %= <<= ||= &&=), global variables ($g, plain and compound). - Methods: required / optional /
*splat/ keyword (a:,b: 2) /**rest/&blockparameters, setter defs (def name=), endless methods (def foo = expr), singleton method defs on any object (def obj.foo/def Const.foo), recursion,return,super. - Blocks / Procs / lambdas:
{ }/do…endclosures,yield,block_given?,&blockcapture, block params with destructuring (|(a, b)|) and rest (|*rest|,|head, *rest|), numbered params (_1/_2) andit,Proc/lambda/stabby->(){},&procblock-pass andSymbol#to_proc(the&:symshorthand). - Classes & modules: inheritance,
@ivars,@@class variables(shared down the superclass hierarchy),new/initialize, constants and constant assignment, class methods (def self.foo), modules +include/prepend(mixins, with full ancestor-chainsuperthrough included/prepended modules and the singleton chain),Module#ancestors/include?,attr_accessor/reader/writer,Struct.new. - Metaprogramming: dynamic dispatch via mutable method tables,
method_missing,send/public_send,respond_to?,define_method,instance_eval/instance_exec,class_eval/module_eval/class_exec,instance_variable_get/set/defined?, stringeval(the embedded front-end compiling Ruby at runtime),Binding(binding,Binding#eval,eval(str, binding),local_variable_get/set/defined?,local_variables,receiver— capturing a frame's locals so eval'd code reads and writes them), and the class/module hooksinherited/included/prepended/method_added/extended. - Runtime loading:
require/require_relativeload, compile and run a.rbfile once (relative + search-path resolution,LoadErroron miss,true/falsereturn) — the embedded front-end loading code at runtime. - Strings: mutable (reference semantics) with
<</concat/replace/prepend/ insert/[]=/slice!/the bang methods andfreeze/FrozenError; interpolation, heredocs (<</<<-/<<~),%w/%iand%q/%Q/%W/%Iliterals, the\a/\b/\v/\f/\s/\n/\t/\r/\e/\0escapes,%/format/sprintf, case/strip/split/each_char/lines/succ(next) and friends. - Regular expressions:
/re/imxliterals,Regexp/MatchData,=~/match/match?/scan/gsub/sub/split, and the match globals$~/$1..$N/$&/$`/$'— running on the standalone pure-Go go-ruby-regexp engine, so the build stays CGO=0. - Standard library leaves:
JSON(generate/dump/pretty_generate/parse+Object#to_json, with object key order preserved and MRI-matching number/escape formatting),Digest(MD5/SHA1/SHA256/SHA512—hexdigest/digest/base64digest),Base64, andZlib(crc32/adler32+Deflate/Inflate) — eachrequire-able and pure-Go. Marshal(dump/load/restore+MAJOR_VERSION/MINOR_VERSION) — Ruby's binary serialization, byte-for-byte identical to MRI across Integer/Bignum, Float, Symbol, String, Array, Hash (incl. defaults), and the symbol/object-link tables (shared objects and cycles round-trip). Runs on the standalone pure-Go go-ruby-marshal engine, so the build stays CGO=0.- File & Random:
File— path helpers (basename/dirname/extname/join/split/expand_path) and filesystem ops (read/write/exist?/file?/directory?/size/delete), raisingErrno::ENOENTfor missing paths;Random— a bit-exact reimplementation of MRI's seeded MT19937, soRandom.new(seed)/srand+randreproduce MRI's sequence. - IO:
IOwith$stdout/$stderr/STDOUT/STDERR/$stdinas real objects (write/<</print/puts/printf/putc/sync/flush/close),StringIO(require "stringio") — an in-memory IO with the full read side (read/gets/getc/readline/readlines/each_line/each_char,pos/seek/rewind/truncate/eof?/string), andKernel#warn.Kernel#puts/print/pwrite through the current$stdout, so reassigning it to aStringIOcaptures output, as in MRI.File.open(modesr/w/a/r+, block-scoped auto-close) returns a file-backed IO (File<IO) with the same read/write protocol, plusFile.readlines/File.foreach. Dir:entries/children/glob/[],exist?/empty?,pwd/home,mkdir/rmdir/chdir(block-scoped),each_child/foreach, raisingErrno::ENOENT/Errno::EEXISTas MRI does.- Collections: Array / Hash / Range with
Enumerable(map/select/reduce/minmax/…) andComparable, both written once in embedded Ruby; Array bang methods (map!/sort!/select!/reject!/compact!/uniq!/reverse!), structural/combinatorial ops (transpose/product/combination/to_h), theHash[…]constructor, String ranges (("a".."e")iterating viaString#succ), andRange#step/Integer#step(integer and float walks, both directions). - Enumerator: every blockless iterator (
each/map/select/reject/each_slice/each_cons/each_with_index/times/upto/each_char/…) returns anEnumerator(MRI semantics) withnext/peek/rewind/size/to_a,with_index/each_with_index,Kernel#enum_for/to_enum, and fullEnumerablechaining ([1,2,3].map.with_index { |x, i| … }); plusEnumerator::Lazy(lazy) — deferredmap/collect,select/filter,reject,filter_map,flat_map/collect_concat,grep/grep_v,zip,uniq,compact,with_index,take/take_while,drop/drop_whileover finite or infinite ((1..Float::INFINITY).lazy) sources, chained without materialising and pulled byfirst(n)/each/to_a/force. - Numeric tower:
Integer/Float/Rational/Complexunder a sharedNumeric(carryingComparable);Module#ancestors/include?,Class#superclass. - Objects:
dup/clone/freeze/frozen?,equal?,object_id/__id__(MRI's deterministic immediate-value ids, stable per reference object),instance_variable_get/set. - Math: the
Mathmodule (sqrt/cbrt/exp/log/log2/log10, the trig and hyperbolic functions,atan2/hypot/pow) withMath::PI/Math::E. - NDArray: a NumPy-style n-dimensional array —
zeros/ones/full/arange/from, element-wise+ - * /with scalar broadcasting, ufuncs (sqrt/exp/log/sin/cos/abs), reductions (sum/mean/max/min/prod/argmax/argmin),matmul/dot,transpose/reshape/flatten,shape/to_a/[]— binding the pure-Go go-ndarray library, no cgo / no NumPy. - Image: a scikit-image-style image processor —
Image.new/load/save, pixelget/set, filters (gaussian_blur/box_blur/median/sharpen), edges (sobel/prewitt/scharr/laplacian/canny), morphology (erode/dilate), geometry (resize/rotate90/crop/flip_*), colour (grayscale/invert/rgb_to_hsv/otsu) — binding the pure-Go go-images library, no cgo. - FFT: an
FFTmodule — 1-D (fft/ifft/rfft/irfft), N-D and 2-D (fftn/ifftn/fft2/ifft2), bin-frequency helpers (fftfreq/rfftfreq), window functions (hann/hamming/blackman/blackman_harris/bartlett), and spectral helpers (psd/spectrogram) — binding the pure-Go go-fft library, anumpy.fft-style transform with no cgo / no FFTW, returningComplexspectra. - Set: Ruby's
Set—new/[],add(<<)/add?/delete/merge/clear,include?/member?/size/length/count/empty?,subset?/superset?,union(|)/intersection(&)/difference(-),each/to_a/to_set— binding the pure-Go go-composites/set library. - Time: Ruby's
Time—now/at/parse, arithmetic (+/-/<=>),strftime/strptime,year/month/mday/hour/min/sec/wday, weekday predicates (monday?…sunday?),utc/getutc/zone,to_i/to_f— binding go-composites/time. - Date: Ruby's
Date—new/parse,+/-/<</>>andnext_day/prev_day/next_month/prev_month,year/month/mday/wday/yday/cwday,leap?, comparisons — binding go-composites/date. - BigDecimal: arbitrary-precision decimal —
+ - * / **,sqrt/abs/ceil/floor/round/pow,to_f/to_i,zero?, comparisons — binding go-composites/bigfloat. - Bag: a multiset / counter (element → multiplicity) —
add(<<)/delete,count/size/distinct/most_common,union/difference/intersection,include?/each/to_a— binding go-composites/bag.
Beyond the stdlib, rbgo binds a set of native capability modules — each
require-able and backed by a canonical pure-Go go-ruby-* library, so the
build stays CGO=0. These wrap real I/O / network / crypto engines, so their
throughput tracks the underlying driver rather than the interpreter:
-
Databases & key-value stores:
mysql2(require "mysql2"—Mysql2::Client,Resultand preparedStatement, over go-ruby-mysql, a pure-Go mysql2 overgo-sql-driver/mysql);mongo+bson(require "mongo"—Mongo::Client/Database/Collection/Cursorwith orderedBSONdocuments andBSON::ObjectId, over go-ruby-mongodb);etcd/etcdv3(require "etcd"— the etcd v3 KV client: get/put/txn/lease/watch/lock, over go-ruby-etcd); andbolt(require "bolt"— the embedded bbolt B+tree store:DB/Tx/Bucket/Cursor, files interoperable with reference bbolt tooling, over go-ruby-bbolt overgo.etcd.io/bbolt). -
Messaging:
nats(require "nats"— NATS publish / subscribe / request with headers, over go-ruby-nats); andkafka(require "kafka"— a ruby-kafka-faithful producer / consumer / admin client, over go-ruby-kafka overtwmb/franz-go). -
RPC, serialization & columnar data:
grpc(require "grpc"— a gRPC server and client stub sharing an in-process transport, so one program can be both peers, over go-ruby-grpc);google/protobuf(require "protobuf"— Protocol Buffers with wire-compatibleencode/decode/encode_json, well-known types and the descriptor pool, over go-ruby-protobuf);arrow(require "arrow"— Apache ArrowDataType/Field/Schema/Array/RecordBatch/Table, over go-ruby-arrow); andparquet(require "parquet"— an Arrow-backed Parquet reader / writer, over go-ruby-parquet). -
Web & security:
faraday(require "faraday"— the Faraday HTTP client: connection / request / response / middleware, over go-ruby-faraday);puma(require "puma"— a Rack HTTP server drivingapp.callunder the emulated GVL, over go-ruby-puma);graphql(require "graphql"— a graphql-ruby-flavoured type system + query execution preserving the exactdata/errorsshape, over go-ruby-graphql);saml/ruby-saml(require "saml"— SAML SSO auth requests / responses / metadata / logout, exposed asSAMLandOneLogin::RubySaml, over go-ruby-saml);webauthn(require "webauthn"— WebAuthn / FIDO2 relying-party registration and authentication, over go-ruby-webauthn);acme(require "acme"— the ACME / Let's EncryptAcme::Clientaccount / order / authorization / challenge / certificate flow, over go-ruby-acme);rbnacl(require "rbnacl"— NaCl / libsodiumSecretBox/Box, Ed25519 / Curve25519 keys, AEAD, hashing and password hashing, over go-ruby-sodium); andage(require "age"— age file encryption with X25519 and scrypt recipients, interoperable with the referenceageCLI, over go-ruby-age overfilippo.io/age). -
Documents & observability:
prawn(require "prawn"— the Prawn PDF document DSL, emitting well-formed PDF 1.3+, over go-ruby-prawn overgo-pdf/fpdf);bleve(require "bleve"— full-text search: index / mapping / query / facets, over go-ruby-bleve); andopentelemetry(require "opentelemetry"— distributed tracing: tracer / span / W3C context and the SDK exporter seam over the OpenTelemetry Go SDK, over go-ruby-opentelemetry). -
AOT compiler (
rbgo build): lowers a program's methods to native Go and links a specialised binary. Pure integer methods become unboxedint64kernels with an overflow/÷0deopt back to the interpreter — the generatedfib(30)runs ~4× faster than MRI+YJIT while staying correct for every input. See docs/aot-compiler.md. -
Closed-world binary (
rbgo build --closed): bakes the whole program in as bytecode (and loads the prelude from frozen bytecode), then drops the lexer/parser/compiler from the link. The result runs with no source file and no front-end — a smaller, self-contained binary.rbgo buildreports whicheval/requirecalls (if any) would raise in the closed binary, since there is no front-end left to compile source at runtime. -
WebAssembly (
GOOS=js GOARCH=wasm) is a supported target. rbgo runs in the browser two ways: the playground ships the full interpreter (front-end + VM + numeric/image stack) as a wasm module that evaluates arbitrary Ruby in the page (see Run in the browser); andrbgo build --closed --target wasm app.rbcross-compiles a closed-world wasm module that runs that one program (no front-end linked) and can drive the page's DOM/Canvas through the built-inJSmodule (JS.document/JS.log/JS::Ref#call/JS.raf).
100% coverage is enforced in CI across all six 64-bit targets (amd64, arm64,
riscv64, loong64, ppc64le, s390x) and three OSes. Phase 8 (conformance and
representation/perf tuning) is well advanced: the conformance campaign above
brought the front-end to ~100 % parse / 99.82 % parse+compile on real-world Ruby,
and on the performance side small-integer interning and capture-tracked
frame-environment recycling have cut call-path allocations (a small-int loop from
~245k allocations to 1; recursion's call allocations halved, ~14% faster), with
the 6-runtime benchmark suite (BENCHMARKS.md) tracking rbgo vs
MRI / YJIT / JRuby / TruffleRuby. The road from "parses + compiles" to "runs whole
applications" — the runtime stdlib + C-extension surface — is now well underway:
the real puppet apply CLI runs end-to-end (see Running Puppet above),
converging notify, file and exec resources and exiting cleanly, with the
broader resource providers (package / service) the active next milestone.
See the
roadmap.
Requires Go 1.26.4+.
# run a one-liner
go run ./cmd/rbgo run -e 'puts 1 + 2' # => 3
# run a file
cat > fib.rb <<'RB'
def fib(n)
if n < 2
n
else
fib(n - 1) + fib(n - 2)
end
end
puts fib(20)
RB
go run ./cmd/rbgo run fib.rb # => 6765
# build the CLI
go build -o rbgo ./cmd/rbgo
./rbgo fib.rb
# AOT-compile a program's methods to native code and link a specialised binary
./rbgo build -o fib fib.rb # fib (the method) becomes native int64
./fib fib.rb # => 6765
# closed-world: bake the program in as bytecode and drop the front-end
./rbgo build --closed -o fib fib.rb # no lexer/parser/compiler linked
./fib # => 6765 (runs with no source file)
# WebAssembly: cross-compile a closed-world program to a browser wasm module
./rbgo build --closed --target wasm -o app.wasm app.rb # GOOS=js GOARCH=wasmGOOS=js GOARCH=wasm is a first-class target. Everything is pure Go with cgo
disabled, so the interpreter, the numeric stack and the cgo-free image pipeline
compile to a single wasm module and run entirely in the browser — there is no
server-side code. There are two ways to ship Ruby to the browser:
js/wasm excludes the network/OS gem backends. A browser has no TCP sockets and no cgo, so the gems whose backends open real sockets or use OS facilities are compiled out of the
GOOS=js GOARCH=wasmbuild (they carry a//go:build !(js && wasm)tag, which also keeps ~90 MB of driver code out of the module). On wasm,requireof any of them raises a clean RubyLoadError(cannot load such file -- kafka (not available in the wasm build)):grpc,nats,kafka,mysql2/mysql,mongo/bson,arrow,parquet,openstack,sidekiq,resque(the last two are theredis-backed job queues). A second tier —sqlite3,sequel,activerecord,bolt,bleve,etcd— stillrequires successfully but raises when a connection/handle is constructed (e.g.SQLite3::Database.new), because their drivers do not build for wasm. The native build is unchanged: every gem is present, loads and conforms exactly as before. Plain Ruby, the numeric/image stack and theJSDOM/Canvas bridge are fully available in the browser.
1. The playground — full interpreter in wasm. A self-contained page (Ruby
REPL + a load→gaussian_blur/sobel/canny→render image demo) lives in
web/. It builds cmd/wasm, which links the whole front-end (lexer,
parser, compiler) and VM, so the page can evaluate arbitrary Ruby typed by the
user:
./web/build.sh serve # build web/rbgo.wasm and serve http://localhost:8080The module publishes rbgoEval(src) and rbgoImage(src, bytes) on the JS global
object; see web/README.md for the bridge.
2. rbgo build --target wasm — a closed-world wasm app. To ship one Ruby
program (not a REPL), AOT-bake it into a closed-world wasm module that drops the
front-end:
./rbgo build --closed --target wasm -o app.wasm app.rb--target wasm requires --closed (the wasm entry runs the embedded program,
then parks the Go runtime with select{} so JS callbacks keep firing). The
program can reach the page through the built-in JS module — JS.global,
JS.window, JS.document, JS.log, JS::Ref#get/set/call/[]/on for
DOM and Canvas, and JS.raf { |t| … } for an animation loop — so a closed-world
wasm app can render and handle events with no JavaScript of its own. Serve the
emitted app.wasm next to Go's wasm_exec.js loader.
cmd/rbgo/ CLI: run, build (+ build --closed [--target wasm]; repl later)
cmd/wasm/ GOOS=js GOARCH=wasm playground front-end (see web/) + native stub
cmd/aotgen/ regenerates the AOT differential suite (go:generate)
cmd/freeze-prelude/ regenerates the frozen prelude bytecode (go:generate)
web/ browser playground: index.html, build.sh (rbgoEval/rbgoImage)
internal/
compiler/ AST → bytecode (ISeq), local-slot resolution
bytecode/ instruction set + ISeq
vm/ stack-machine interpreter, arithmetic, builtins
(front-end isolated behind the rbgo_closed build tag)
aot/ AOT compiler: bytecode → Go (level-1/3 kernels, FreezeISeq)
object/ Value interface + concrete value types
docs/ plan-rbgo.md (the roadmap), aot-compiler.md
go test ./...
go test -coverpkg=./internal/... -coverprofile=cov.out ./internal/...
go tool cover -func=cov.out | tail -1If a parent go.work is present, prefix commands with GOWORK=off.
Per-PR CI is a fast native gate: the -race + 100 %-coverage suite on
ubuntu/macos/windows plus native amd64 and arm64 lanes. Because rbgo is
pure-Go (CGO=0, no assembly), the four exotic 64-bit targets
(riscv64/loong64/ppc64le/s390x) are validated off the per-PR critical
path — nightly under QEMU, and on real hardware via the GCC Compile Farm
(ppc64le/riscv64/loong64) plus a LinuxONE s390x host. The real-hardware
workflow is scheduled + secret-gated (inert until credentials are added) to
respect the cfarm acceptable-use policy. See docs/CI.md.
Correctness is judged against independent reference implementations of
Ruby 4.0 — MRI (CRuby) 4.0.5 and JRuby, with TruffleRuby being added
as a third reference (conformance + performance). The differential oracle
scripts/oracle.sh runs a snippet through rbgo, MRI and
JRuby and flags any divergence:
scripts/oracle.sh -e 'p (1..10).select(&:even?).map { |x| x**2 }'Beyond synthetic tests, the bar is real-world Ruby: idioms and test suites
from reference applications — Ruby on Rails (ActiveSupport's pure-Ruby
core_ext) and OpenVox/Puppet (Ruby-heavy manifest evaluation) — drive the
remaining work by demand and double as conformance corpora and performance
baselines (pure-Go CGO=0 vs CRuby's C, JRuby's JVM JIT and TruffleRuby's Graal).
The heavyweight front-end (parse + compile) conformance results — Rails 99.82 %,
Puppet 100 %, ~100 % of all valid Ruby parsed, plus the per-library table — are
in CONFORMANCE-RAILS-PUPPET.md,
CONFORMANCE-LIBRARIES.md and
CONFORMANCE-RSPEC.md; reproduce with
scripts/conformance/heavyweight/sweep.sh.
On top of the front-end sweeps, a ruby/spec ratchet runs the language/ and
core/ suites of ruby/spec — the executable
specification of the language — through rbgo under a minimal MSpec-compatible
shim, and gates CI on a shrink-only floor of passing examples
(scripts/conformance/rubyspec/, floor in
FLOOR). The floor can only be raised, so measured language conformance moves in
one direction. Run it with scripts/conformance/rubyspec/run.sh.
See docs/plan-rbgo.md for the full architecture, the 9-phase plan (Phase 0 vertical slice → Phase 8 conformance & performance), the risk register, and the decision journal. The regexp engine is developed separately as a pure-Go reimplementation of Onigmo in go-ruby-regexp/regexp.
BSD-3-Clause. See LICENSE.
