Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
55 changes: 46 additions & 9 deletions internal/vm/kernel_introspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
67 changes: 67 additions & 0 deletions internal/vm/ruby_namespace_test.go
Original file line number Diff line number Diff line change
@@ -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"},
})
}
Loading