From 185cec7b2d8db760e952035d885a15b033ef030d Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 31 Oct 2026 21:29:52 +0100 Subject: [PATCH] vm: the Ruby 4.0 namespace, as one object under two names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby 4.0 added a `Ruby` module holding the nine version constants that until then existed only as `RUBY_`-prefixed globals. rbgo had none of it, so `core/builtin_constants/builtin_constants_spec.rb` errors on ten examples the moment the suite is measured at 4.0 — which is where this project is going (#799, decided: parity with the latest MRI, 4.0.9). ## ⛔ The point is IDENTITY, not equality ruby/ruby's version.c does not define the pair twice. It defines each value once and installs it under both names in one step (tag v4.0.9 — note the tag naming changed at 4.0.0, from `v3_4_1` to `v4.0.9`): static inline void define_ruby_const(VALUE mod, const char *name, VALUE value, bool toplevel) { if (toplevel) { rb_define_global_const(name, value); // RUBY_VERSION name += rb_strlen_lit("RUBY_"); } rb_define_const(mod, name, value); // Ruby::VERSION } which is why the spec asserts `Ruby::VERSION.should.equal?(RUBY_VERSION)` — `equal?` is identity. An implementation that built two frozen strings with the same bytes would satisfy `==` and fail all nine, and the failure would read as "the constant has the wrong value", which it would not. So there is a `define()` closure here rather than eighteen assignments: the value is built by one expression and the two names cannot drift apart later. `ENGINE_VERSION` shares the VERSION object, as version.c shares its one `version` VALUE between them; nothing in ruby/spec requires that either way, and matching it leaves a reader comparing against the reference with nothing to explain. `DESCRIPTION` is the ninth and is defined separately upstream (`define_ruby_description`, removed and redefined when a JIT is switched on) — here it is one of the nine because rbgo's is static. ## Measured — three runs, not one subtraction `core/builtin_constants/builtin_constants_spec.rb`, darwin/arm64, corpus at the pinned SPEC_SHA 87b1631: | | shim `CUR_VERSION` | binary | result | |---|---|---|---| | A | 3.4.1 | this branch | `pass=17 fail=0 error=0` | | B | 4.0.9 | this branch | **`pass=27`** `fail=0 error=0` | | C | 4.0.9 | `main` (3a3fb818) | `pass=17` **`error=10`** | - **A is the no-regression row**: `BASELINE` records `17` for this file, and this branch produces 17 at the version the gate actually measures at. Nothing the ratchet can see changes. - **B is the gain**: +10, and every one of them passes. - **C is the control**, and without it B proves nothing. On `main` the same ten examples error with `uninitialized constant Ruby` — so the measurement can see the thing it is looking for, and the ten in B are those ten. The gain is therefore real and **currently invisible to CI**, deliberately: the shim resolves `ruby_version_is` at the reported 3.4.1, so these land before the version moves rather than after. That order matters — #799 asks for the version move to be made with the baseline NOT regenerated, so the ratchet names every file that shifts in both directions, and a file shifting because a capability was missing is noise in that reading. ## Tests `internal/vm/ruby_namespace_test.go` asserts the nine identities as nine separate lines with nine `true`s in the expected output — a loop over a list of names that silently iterated eight would print `true` eight times and look like a pass, so the count is part of the assertion. It also carries its own control, `TestEqualIsIdentityNotEquality`: if `equal?` answered on value in this VM, every identity assertion above would pass whatever the implementation did. Two separately-built strings are `==` and not `equal?`; one object is both. ## Security and performance No new input is parsed and no bound changes: nine map entries written once at VM start-up, and one fewer String allocated than before (ENGINE_VERSION now shares VERSION's). `go vet` clean, `gofmt` clean. README: the `RUBY_VERSION` row of the MRI-differences table now records that the namespace exists, that the gate cannot count it at 3.4.1, and the 17/27 figures. go-embedded-ruby/docs#41 states the same thing on the documentation side and makes the stated version a checked claim. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- internal/vm/kernel_introspect.go | 55 ++++++++++++++++++++---- internal/vm/ruby_namespace_test.go | 67 ++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 internal/vm/ruby_namespace_test.go diff --git a/README.md b/README.md index da72e063..72e1097e 100644 --- a/README.md +++ b/README.md @@ -751,7 +751,7 @@ Otherwise: | | how it differs from MRI 4.0.5 | | --- | --- | -| `RUBY_VERSION` | rbgo reports `"3.4.1"`; the differential oracle it is developed against is MRI 4.0.5 | +| `RUBY_VERSION` | rbgo reports `"3.4.1"`; the differential oracle it is developed against is MRI 4.0.5, and the latest MRI release is 4.0.9 (2026-10-23). The Ruby 4.0 **`Ruby` namespace** *is* implemented — all nine of `Ruby::{VERSION,RELEASE_DATE,PLATFORM,PATCHLEVEL,REVISION,COPYRIGHT,ENGINE,ENGINE_VERSION,DESCRIPTION}` are the **same objects** as their `RUBY_`-prefixed globals, as `version.c` defines them, so `Ruby::VERSION.equal?(RUBY_VERSION)` is `true`. The conformance shim resolves `ruby_version_is` at the reported `3.4.1`, so the ten examples asserting that in `core/builtin_constants` are **not counted by the gate**: measured here, that file yields 17 at 3.4.1 and 27 at 4.0.9 ([#799](https://github.com/go-embedded-ruby/ruby/issues/799)) | | `Numeric#to_int` | not defined — `Complex(2.9, 0).to_int` raises `NoMethodError`; MRI returns `2` | | `File::Stat#==` | `a == b` is `false` for two stats of the same file (MRI: `true`); `a.==(b)` answers `true` | | `File#stat` on a file unlinked while open | raises `Errno::ENOENT`; MRI answers from the open descriptor. rbgo's streams are buffered by path and hold no descriptor | diff --git a/internal/vm/kernel_introspect.go b/internal/vm/kernel_introspect.go index 32ada5f0..11df7471 100644 --- a/internal/vm/kernel_introspect.go +++ b/internal/vm/kernel_introspect.go @@ -33,27 +33,64 @@ func Description() string { return "ruby " + rubyVersion + " [" + rubyPlatform() func (vm *VM) registerVersionConstants() { version := rubyVersion + + // The Ruby module, new in Ruby 4.0: "the Ruby module that contains portable + // information among implementations. The constants defined here are aliased + // in the toplevel with +RUBY_+ prefix" (version.c, Init_version at tag + // v4.0.9). + // + // ⛔ ONE OBJECT, TWO NAMES — this is the whole of it. version.c does not + // define the pair twice; it defines each value once and installs it under + // both names: + // + // static inline void + // define_ruby_const(VALUE mod, const char *name, VALUE value, bool toplevel) + // { + // if (toplevel) { + // rb_define_global_const(name, value); // RUBY_VERSION + // name += rb_strlen_lit("RUBY_"); + // } + // rb_define_const(mod, name, value); // Ruby::VERSION + // } + // + // which is why core/builtin_constants asserts `Ruby::VERSION.should.equal? + // (RUBY_VERSION)` — `equal?` is IDENTITY, not equality. Two separate frozen + // strings holding the same bytes would satisfy `==` and fail every one of + // those nine examples, and the failure would read as "the constant is + // wrong" rather than "it is a different object". Hence define() below: the + // value is built once, by one expression, and the two names cannot drift. + mRuby := newClass("Ruby", nil) + mRuby.isModule = true + vm.consts["Ruby"] = mRuby + define := func(name string, v object.Value) { + vm.consts["RUBY_"+name] = v + mRuby.consts[name] = v + } + // Every RUBY_* String constant is frozen. version.c's Init_version at tag // v3_4_0 builds each one through // #define MKSTR(name) rb_obj_freeze(rb_usascii_str_new_static(ruby_##name, sizeof(ruby_##name)-1)) // rb_define_global_const("RUBY_VERSION", MKSTR(version)); // so core/builtin_constants asserts `.should.frozen?` on all nine; rbgo only // froze RUBY_RELEASE_DATE and RUBY_REVISION. - vm.consts["RUBY_VERSION"] = object.NewFrozenStringView(version) - vm.consts["RUBY_ENGINE"] = object.NewFrozenStringView("ruby") - vm.consts["RUBY_ENGINE_VERSION"] = object.NewFrozenStringView(version) - vm.consts["RUBY_PATCHLEVEL"] = object.IntValue(0) - vm.consts["RUBY_PLATFORM"] = object.NewFrozenStringView(rubyPlatform()) - vm.consts["RUBY_DESCRIPTION"] = object.NewFrozenStringView(Description()) - vm.consts["RUBY_COPYRIGHT"] = object.NewFrozenStringView("ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto") + versionStr := object.NewFrozenStringView(version) + define("VERSION", versionStr) + define("ENGINE", object.NewFrozenStringView("ruby")) + // The SAME object as VERSION, as in version.c, which passes the one `version` + // VALUE to both: `rb_define_const(mRuby, "ENGINE_VERSION", version)`. + define("ENGINE_VERSION", versionStr) + define("PATCHLEVEL", object.IntValue(0)) + define("PLATFORM", object.NewFrozenStringView(rubyPlatform())) + define("DESCRIPTION", object.NewFrozenStringView(Description())) + define("COPYRIGHT", object.NewFrozenStringView("ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto")) // RUBY_RELEASE_DATE and RUBY_REVISION describe the MRI release this runtime // targets, and both are frozen Strings (core/builtin_constants asserts the // class and the frozen-ness of each). version.h at tag v3_4_1 builds // RUBY_RELEASE_DATE out of RUBY_RELEASE_YEAR/MONTH/DAY and takes RUBY_REVISION // from the generated revision.h, i.e. the tagged commit: ruby/ruby v3_4_1 is // 48d4efcb85000e1ebae42004e963b5d0cedddcf2, committed 2024-12-25. - vm.consts["RUBY_RELEASE_DATE"] = object.NewFrozenStringView(releaseDate) - vm.consts["RUBY_REVISION"] = object.NewFrozenStringView(revision) + define("RELEASE_DATE", object.NewFrozenStringView(releaseDate)) + define("REVISION", object.NewFrozenStringView(revision)) vm.registerRbConfig(version) } diff --git a/internal/vm/ruby_namespace_test.go b/internal/vm/ruby_namespace_test.go new file mode 100644 index 00000000..b7eda54e --- /dev/null +++ b/internal/vm/ruby_namespace_test.go @@ -0,0 +1,67 @@ +package vm_test + +import "testing" + +// The Ruby module, new in Ruby 4.0: "the Ruby module that contains portable +// information among implementations. The constants defined here are aliased in +// the toplevel with +RUBY_+ prefix" (ruby/ruby version.c, Init_version at tag +// v4.0.9 -- note the tag naming changed at 4.0.0, from v3_4_1 to v4.0.9). +// +// ⛔ THESE ARE IDENTITY TESTS, NOT EQUALITY TESTS, and that is the only reason +// they are interesting. version.c installs each value under both names in one +// step: +// +// if (toplevel) { +// rb_define_global_const(name, value); // RUBY_VERSION +// name += rb_strlen_lit("RUBY_"); +// } +// rb_define_const(mod, name, value); // Ruby::VERSION +// +// so core/builtin_constants/builtin_constants_spec.rb asserts +// `Ruby::VERSION.should.equal?(RUBY_VERSION)` for all nine. An implementation +// that built two frozen strings with the same bytes would pass `==` and fail +// every one of those nine examples -- and the failure would read as "the +// constant has the wrong value", which it does not. +// +// The nine are spelled out rather than looped over a list of names: a loop that +// silently iterated eight would print `true` eight times and look exactly like +// a pass. Nine lines, nine `true`s in the expected output, so the COUNT is part +// of what is asserted. +func TestTheRubyNamespaceAliasesTheSameObjects(t *testing.T) { + checkCases(t, []runCase{ + {`p Ruby.instance_of?(Module)`, "true\n"}, + { + `p Ruby::VERSION.equal?(RUBY_VERSION) +p Ruby::RELEASE_DATE.equal?(RUBY_RELEASE_DATE) +p Ruby::PLATFORM.equal?(RUBY_PLATFORM) +p Ruby::PATCHLEVEL.equal?(RUBY_PATCHLEVEL) +p Ruby::REVISION.equal?(RUBY_REVISION) +p Ruby::COPYRIGHT.equal?(RUBY_COPYRIGHT) +p Ruby::ENGINE.equal?(RUBY_ENGINE) +p Ruby::ENGINE_VERSION.equal?(RUBY_ENGINE_VERSION) +p Ruby::DESCRIPTION.equal?(RUBY_DESCRIPTION)`, + "true\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\ntrue\n", + }, + // version.c passes the one `version` VALUE to both VERSION and + // ENGINE_VERSION, so in MRI these are the same object too. Nothing in + // ruby/spec requires it either way; matching it is free, and a reader who + // checks against the reference should find no difference to explain. + {`p RUBY_VERSION.equal?(RUBY_ENGINE_VERSION)`, "true\n"}, + // The values still have to be the values. An alias to the wrong object + // would satisfy every identity above. + {`p Ruby::VERSION == RUBY_VERSION && Ruby::VERSION.frozen?`, "true\n"}, + }) +} + +// ⛔ The control for the tests above: `equal?` must actually be identity in this +// VM. If it answered on value, every assertion above would pass whatever the +// implementation did, and the nine examples in ruby/spec would still fail. +// +// Two separately-built strings with the same bytes are `==` and not `equal?`; +// one object is both. Without this, the test file above is nine assertions +// resting on an untested assumption about the matcher. +func TestEqualIsIdentityNotEquality(t *testing.T) { + checkCases(t, []runCase{ + {`a = "x" + "y"; b = "x" + "y"; p a == b, a.equal?(b), a.equal?(a)`, "true\nfalse\ntrue\n"}, + }) +}