ruby

package module
v0.0.0-...-aaf2ed9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

go-embedded-ruby/ruby

ruby — go-embedded-ruby

Docs License Go Phase

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

Ecosystem — rbgo composes the go-ruby-* family

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 127 standalone pure-Go modules — all CI-green, 100% coverage, 6-arch — each its own org. The go.mod currently binds 104 into rbgo as native modules (binding the rest in progress):

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): abbrev, acme, activerecord, addressable, age, arrow, base64, bbolt, bcrypt, benchmark, bigdecimal, bleve, builder, cgi, chronic, cmath, commonmark, csv, date, did-you-mean, digest, dotenv, dry-struct, dry-types, dry-validation, erb, etcd, faker, faraday, find, format, getoptlong, grape, graphql, grpc, haml, hcl2, i18n, ipaddr, jbuilder, json, jwt, kafka, kramdown, liquid, logger, mail, marshal, matrix, mime-types, minitest, money, mongodb, msgpack, mustache, mysql, nats, nokogiri, oauth2, observer, oidc, opentelemetry, optparse, ostruct, parquet, parser, pathname, pg, prawn, prettyprint, prime, protobuf, pstore, public-suffix, puma, rack, redis, regexp, resolv, rexml, rouge, rqrcode, rspec, rss, rubocop, saml, scanf, securerandom, sequel, set, shellwords, sinatra, slim, sodium, sqlite3, strscan, toml, tsort, tzinfo, unicode-normalize, uri, webauthn, xslt, yaml, 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).

Status

Conformance & performance (campaign final, 2026-06-27)

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 .rb files). 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 .rb files — 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 real puppet apply CLI runs end-to-end under rbgo (see Running Puppet below), converging notify, file and exec resources; the broader resource providers (package / service host 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.

Running Puppet — puppet apply runs end-to-end

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::CommandLinePuppet::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-based Exception#backtrace / set_backtrace / full_message, interpolated regexp literals, non-local block return, Symbol#intern, NilClass conversions, Array slice-assignment, Module.new (real anonymous module running its block as a body), extend transitivity (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 real OptionParser (optparse), File::Stat / FileTest + on-disk filesystem operations, objspace, and more — each require-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 apply CLI converging notify, file and exec resources through the full pipeline (boot → parse → compile → transaction / RAL → output → clean exit). The next frontier is the broader resource providersuser / group are provider-ready, while package / service need 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

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 literals 0x/0o/ bare-0-octal/0b/0d with underscores), floats, strings, symbols (incl. operator-method symbols :+/:<</:[]=/:<=>, usable with reduce(:+)/inject/send(:+, x)), arrays, hashes, ranges (incl. beginless/endless), Complex and Rational numbers, 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. modifier rescue, expr rescue fallback), begin/rescue/else/ensure/retry, break/next, Kernel#loop, and Fiber (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?) and Queue (blocking push/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 => name binding suffix, guards (if/unless), the one-line forms expr => pattern and expr in pattern, and NoMatchingPatternError — via the deconstruct/deconstruct_keys protocols.
  • 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 / &block parameters, 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…end closures, yield, block_given?, &block capture, block params with destructuring (|(a, b)|) and rest (|*rest|, |head, *rest|), numbered params (_1/_2) and it, Proc/lambda/stabby ->(){}, &proc block-pass and Symbol#to_proc (the &:sym shorthand).
  • 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-chain super through 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?, string eval (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 hooks inherited/included/prepended/method_added/extended.
  • Runtime loading: require/require_relative load, compile and run a .rb file once (relative + search-path resolution, LoadError on miss, true/false return) — the embedded front-end loading code at runtime.
  • Strings: mutable (reference semantics) with <</concat/replace/prepend/ insert/[]=/slice!/the bang methods and freeze/FrozenError; interpolation, heredocs (<</<<-/<<~), %w/%i and %q/%Q/%W/%I literals, the \a/\b/\v/\f/\s/\n/\t/\r/\e/\0 escapes, %/format/sprintf, case/strip/split/each_char/lines/succ(next) and friends.
  • Regular expressions: /re/imx literals, 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/SHA512hexdigest/digest/base64digest), Base64, and Zlib (crc32/ adler32 + Deflate/Inflate) — each require-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), raising Errno::ENOENT for missing paths; Random — a bit-exact reimplementation of MRI's seeded MT19937, so Random.new(seed) / srand+rand reproduce MRI's sequence.
  • IO: IO with $stdout/$stderr/STDOUT/STDERR/$stdin as 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), and Kernel#warn. Kernel#puts/print/p write through the current $stdout, so reassigning it to a StringIO captures output, as in MRI. File.open (modes r/w/ a/r+, block-scoped auto-close) returns a file-backed IO (File < IO) with the same read/write protocol, plus File.readlines/File.foreach.
  • Dir: entries/children/glob/[], exist?/empty?, pwd/home, mkdir/rmdir/chdir (block-scoped), each_child/foreach, raising Errno::ENOENT/Errno::EEXIST as MRI does.
  • Collections: Array / Hash / Range with Enumerable (map/select/reduce/ minmax/…) and Comparable, both written once in embedded Ruby; Array bang methods (map!/sort!/select!/reject!/compact!/uniq!/reverse!), structural/combinatorial ops (transpose/product/combination/to_h), the Hash[…] constructor, String ranges (("a".."e") iterating via String#succ), and Range#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 an Enumerator (MRI semantics) with next/peek/rewind/size/to_a, with_index/each_with_index, Kernel#enum_for/to_enum, and full Enumerable chaining ([1,2,3].map.with_index { |x, i| … }); plus Enumerator::Lazy (lazy) — deferred map/collect, select/filter, reject, filter_map, flat_map/collect_concat, grep/grep_v, zip, uniq, compact, with_index, take/take_while, drop/drop_while over finite or infinite ((1..Float::INFINITY).lazy) sources, chained without materialising and pulled by first(n)/each/to_a/force.
  • Numeric tower: Integer/Float/Rational/Complex under a shared Numeric (carrying Comparable); 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 Math module (sqrt/cbrt/exp/log/log2/log10, the trig and hyperbolic functions, atan2/hypot/pow) with Math::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, pixel get/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 FFT module — 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, a numpy.fft-style transform with no cgo / no FFTW, returning Complex spectra.
  • Set: Ruby's Setnew/[], 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 Timenow/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 Datenew/parse, +/-/<</>> and next_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, Result and prepared Statement, over go-ruby-mysql, a pure-Go mysql2 over go-sql-driver/mysql); mongo + bson (require "mongo"Mongo::Client/ Database/Collection/Cursor with ordered BSON documents and BSON::ObjectId, over go-ruby-mongodb); etcd/ etcdv3 (require "etcd" — the etcd v3 KV client: get/put/txn/lease/watch/lock, over go-ruby-etcd); and bolt (require "bolt" — the embedded bbolt B+tree store: DB/Tx/Bucket/Cursor, files interoperable with reference bbolt tooling, over go-ruby-bbolt over go.etcd.io/bbolt).

  • Messaging: nats (require "nats" — NATS publish / subscribe / request with headers, over go-ruby-nats); and kafka (require "kafka" — a ruby-kafka-faithful producer / consumer / admin client, over go-ruby-kafka over twmb/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-compatible encode/decode/encode_json, well-known types and the descriptor pool, over go-ruby-protobuf); arrow (require "arrow" — Apache Arrow DataType/Field/Schema/Array/ RecordBatch/Table, over go-ruby-arrow); and parquet (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 driving app.call under the emulated GVL, over go-ruby-puma); graphql (require "graphql" — a graphql-ruby-flavoured type system + query execution preserving the exact data/errors shape, over go-ruby-graphql); saml/ ruby-saml (require "saml" — SAML SSO auth requests / responses / metadata / logout, exposed as SAML and OneLogin::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 Encrypt Acme::Client account / order / authorization / challenge / certificate flow, over go-ruby-acme); rbnacl (require "rbnacl" — NaCl / libsodium SecretBox/Box, Ed25519 / Curve25519 keys, AEAD, hashing and password hashing, over go-ruby-sodium); and age (require "age" — age file encryption with X25519 and scrypt recipients, interoperable with the reference age CLI, over go-ruby-age over filippo.io/age).

  • Documents & observability: prawn (require "prawn" — the Prawn PDF document DSL, emitting well-formed PDF 1.3+, over go-ruby-prawn over go-pdf/fpdf); bleve (require "bleve" — full-text search: index / mapping / query / facets, over go-ruby-bleve); and opentelemetry (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 unboxed int64 kernels with an overflow/÷0 deopt back to the interpreter — the generated fib(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 build reports which eval/require calls (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); and rbgo build --closed --target wasm app.rb cross-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-in JS module (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.

Quick start

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=wasm

Run in the browser — WebAssembly

GOOS=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:

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:8080

The 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 moduleJS.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.

Layout

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

Testing & conformance

go test ./...
go test -coverpkg=./internal/... -coverprofile=cov.out ./internal/...
go tool cover -func=cov.out | tail -1

If a parent go.work is present, prefix commands with GOWORK=off.

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.

Design & roadmap

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.

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package ruby is the public embedding API for go-embedded-ruby: compile and run Ruby source on a fresh VM. It is what an external Go program — or a custom WebAssembly build that bakes a Ruby program in with //go:embed — imports to embed the interpreter without reaching into the internal packages.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(src string, out io.Writer) error

Run parses, compiles and executes src on a new VM, writing the program's output to out. It returns a parse, compile or runtime error, or nil on success. The VM stays alive after Run returns as long as something still references it (e.g. a browser event handler the program registered through the JS bridge), which is what lets an embedded, event-driven program keep running.

Types

This section is empty.

Directories

Path Synopsis
cmd
aotgen command
Command aotgen emits AOT-compiled Ruby methods as real *VM methods into internal/vm/aot_e2e_gen_test.go, each paired with the reference MRI's output for the same program, captured at generation time.
Command aotgen emits AOT-compiled Ruby methods as real *VM methods into internal/vm/aot_e2e_gen_test.go, each paired with the reference MRI's output for the same program, captured at generation time.
freeze-prelude command
Command freeze-prelude regenerates internal/vm/prelude_frozen_gen.go: it compiles the embedded-Ruby prelude (internal/vm/prelude.rb) and writes its bytecode out as a Go literal (embeddedPrelude), so a closed-world binary can load the standard library without linking the front-end.
Command freeze-prelude regenerates internal/vm/prelude_frozen_gen.go: it compiles the embedded-Ruby prelude (internal/vm/prelude.rb) and writes its bytecode out as a Go literal (embeddedPrelude), so a closed-world binary can load the standard library without linking the front-end.
rbgo command
Command rbgo is the CLI front-end for the embedded-ruby interpreter.
Command rbgo is the CLI front-end for the embedded-ruby interpreter.
wasm command
internal
aot
Package aot is the build-time ahead-of-time compiler: it lowers a method's bytecode ISeq to Go source, which the Go toolchain then compiles to native code.
Package aot is the build-time ahead-of-time compiler: it lowers a method's bytecode ISeq to Go source, which the Go toolchain then compiles to native code.
bytecode
Package bytecode defines the instruction set and the compiled unit (ISeq).
Package bytecode defines the instruction set and the compiled unit (ISeq).
compiler
Package compiler lowers the AST to bytecode (an ISeq tree).
Package compiler lowers the AST to bytecode (an ISeq tree).
object
Package object holds the runtime value representation.
Package object holds the runtime value representation.
vm
Code generated by FreezeISeq; DO NOT EDIT.
Code generated by FreezeISeq; DO NOT EDIT.
scripts
conformance/rspec/parsesweep command
Command parsesweep runs every .rb file under one or more roots through rbgo's front-end (parse + compile, NO execution) and reports an acceptance rate.
Command parsesweep runs every .rb file under one or more roots through rbgo's front-end (parse + compile, NO execution) and reports an acceptance rate.
conformance/webapp
Package webapp is a reference-app RUN-conformance harness for rbgo: it proves that the embedded interpreter can actually *run* real Ruby web apps end-to-end (run, not merely parse), by driving the Rack `call(env)` contract directly with a synthetic Rack environment — no TCP socket, no network.
Package webapp is a reference-app RUN-conformance harness for rbgo: it proves that the embedded interpreter can actually *run* real Ruby web apps end-to-end (run, not merely parse), by driving the Rack `call(env)` contract directly with a synthetic Rack environment — no TCP socket, no network.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL