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"}, + }) +}