diff --git a/README.md b/README.md index 20602c44..e1f342bc 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)](LICENSE) [![Go](https://img.shields.io/github/go-mod/go-version/go-embedded-ruby/ruby?label=go&color=00ADD8)](https://go.dev/dl/) [![Release](https://img.shields.io/github/v/release/go-embedded-ruby/ruby?label=release&color=blue)](https://github.com/go-embedded-ruby/ruby/releases/latest) -[![ruby/spec](https://img.shields.io/badge/ruby%2Fspec-23%2C511%20examples%20passing-1a7f37)](#runtime-conformance--rubyspec) +[![ruby/spec](https://img.shields.io/badge/ruby%2Fspec-23%2C673%20examples%20passing-1a7f37)](#runtime-conformance--rubyspec) **A Ruby interpreter written in pure Go, with cgo disabled** — so you can embed it in a Go program with `import "github.com/go-embedded-ruby/ruby"`, or ship it as a single static binary that cross-compiles wherever Go does, with no C toolchain and no libruby. -**How complete is it?** It runs **23,511** of ruby/spec's `language/` + `core/` +**How complete is it?** It runs **23,673** of ruby/spec's `language/` + `core/` examples ([what that counts](#runtime-conformance--rubyspec)) — a large and growing subset of the language, and **not** a drop-in replacement for CRuby. Read [What does not work yet](#what-does-not-work-yet) before you depend on it; the @@ -153,6 +153,14 @@ writing. Only the baseline is machine-checked against this file from the sweep named below and are not derivable from the baseline, which records passing counts and nothing else. +⛔ **The table is a sweep taken at `RUBY_VERSION` 3.4.1**, before the move to +4.0.9. Its rows are left as they were measured rather than rewritten, because +they come from one dated run and are not derivable from the baseline; the badge +and the summary above count the current `BASELINE` and so already reflect 4.0.9. +The move was measured separately — both engines built from one commit minus the +version string — at **+144 passing and 27 examples that went red** +([#799](https://github.com/go-embedded-ruby/ruby/issues/799)). + Measured 2026-09-28 on `862a9f3`, darwin/arm64, against the corpus pinned at `SPEC_SHA=87b1631992bd00cf0c4934474766d54dad088191`, on an isolated snapshot of it so no concurrent run could swap the shim underneath, and reached through a @@ -763,7 +771,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, 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)) | +| `RUBY_VERSION` | rbgo reports `"4.0.9"`, the latest MRI release (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 move from `3.4.1` was measured before it was made, with both engines built from one commit minus the version string: **+144 passing** over `core` and `language`, and **27 examples that went red**, which no pass total shows — see [#799](https://github.com/go-embedded-ruby/ruby/issues/799) for the list of what each of the 27 needs | | `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 | @@ -1248,7 +1256,7 @@ specification of the language — through `rbgo` under a minimal MSpec-compatibl shim, and gates CI on a **per-file** baseline ([`scripts/conformance/rubyspec/`](scripts/conformance/rubyspec/), in `BASELINE`). No file may pass fewer examples than its record or stop loading, so -measured language conformance moves in one direction; the total, **23,511**, is +measured language conformance moves in one direction; the total, **23,673**, is a derived summary rather than the thing gated. Run it with `scripts/conformance/rubyspec/run.sh`, and see *Runtime conformance* under *Status* for the full breakdown. diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index c8d4f318..404fe94a 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -2202,12 +2202,22 @@ func blockParamsAreImplicit(blk *ast.Block) bool { // name AFTER the body is compiled: resolution during the body used the real // name, and the ISeq that remains carries none. // -// The numbered parameters `_1`..`_9` are deliberately NOT hidden. They are named -// in #parameters ([[:opt, :_1]]) and, for the Ruby version rbgo reports -// (RUBY_VERSION 3.4.1), they are visible to a Binding too — ruby/spec pins that -// under `ruby_version_is ""..."4.0"` and pins the hiding only from 4.0. Hiding -// them here would be answering a question rbgo has not yet said it is answering; -// it belongs with the RUBY_VERSION bump. +// ⛔ THE NUMBERED PARAMETERS ARE HIDDEN FROM b.locals AND NOT FROM b.params, and +// that asymmetry is the whole of Ruby 4.0's change here. `_1` leaves +// Binding#local_variables but STAYS in Proc#parameters: +// +// proc { _1 }.parameters # => [[:opt, :_1]] -- ungated, every version +// -> { _1; binding.local_variables }.call("a") +// # => [:_1] below 4.0, [] from 4.0 +// +// numbered_parameters_spec.rb:85 ("affects block parameters") carries no version +// guard at all, so blanking b.params as well would trade four examples for two. +// `it` is blanked from BOTH, because MRI gives it no name in #parameters either +// (`-> { it }.parameters` is [[:req]], NAMELESS). +// +// This was held back until the RUBY_VERSION bump, correctly: below 4.0 +// ruby/spec asserts the OPPOSITE, under `ruby_version_is ""..."4.0"`. The two +// land together. // // ⛔ BOTH KINDS ARE RECORDED, hidden or not. Ruby 4.0 adds // Binding#implicit_parameters, #implicit_parameter_get and @@ -2229,13 +2239,13 @@ func hideImplicitBlockParams(b *builder, blk *ast.Block, positionals []string) { // gate named it, which is the right way to find out. for i, name := range positionals { b.markImplicitParam(i, name) - if name != "it" { - continue - } + // Out of the local table: both kinds, from 4.0. if i < len(b.locals) { b.locals[i] = "" } - if i < len(b.params) { + // Out of #parameters: `it` only. A numbered parameter keeps its name + // there, which numbered_parameters_spec.rb:85 asserts with no guard. + if name == "it" && i < len(b.params) { b.params[i] = "" } } diff --git a/internal/compiler/wave29_kwargs_test.go b/internal/compiler/wave29_kwargs_test.go index 3fa7e792..131b0dfd 100644 --- a/internal/compiler/wave29_kwargs_test.go +++ b/internal/compiler/wave29_kwargs_test.go @@ -360,13 +360,18 @@ func TestImplicitBlockParamsAreHidden(t *testing.T) { if len(writtenIt.Params) != 1 || writtenIt.Params[0] != "it" { t.Errorf("f { |it| it }: Params = %q, want [\"it\"]", writtenIt.Params) } - // The numbered parameters keep theirs, in both tables. + // ⛔ THE NUMBERED PARAMETERS SPLIT THE TWO TABLES, and this asymmetry is the + // whole of Ruby 4.0's change. They keep their names in Params, because + // `proc { _1 }.parameters` is [[:opt, :_1]] under no version guard at all + // (numbered_parameters_spec.rb:85), and lose them in Locals, because from + // 4.0 they are not local variables (:112-135). `it` loses both; a numbered + // parameter loses one. np := blockOf(t, "f { _1 + _2 }") if len(np.Params) != 2 || np.Params[0] != "_1" || np.Params[1] != "_2" { - t.Errorf("f { _1 + _2 }: Params = %q, want [\"_1\" \"_2\"]", np.Params) + t.Errorf("f { _1 + _2 }: Params = %q, want [\"_1\" \"_2\"] -- #parameters is ungated", np.Params) } - if len(np.Locals) < 2 || np.Locals[0] != "_1" || np.Locals[1] != "_2" { - t.Errorf("f { _1 + _2 }: Locals = %q, want the numbered names kept", np.Locals) + if len(np.Locals) < 2 || np.Locals[0] != "" || np.Locals[1] != "" { + t.Errorf("f { _1 + _2 }: Locals = %q, want the numbered names blanked from 4.0", np.Locals) } } diff --git a/internal/vm/binding.go b/internal/vm/binding.go index fc64cf71..d8df9428 100644 --- a/internal/vm/binding.go +++ b/internal/vm/binding.go @@ -292,6 +292,7 @@ func (vm *VM) registerBinding() { cBinding.define("local_variable_get", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value { b := self.(*Binding) name := vm.bindingVarName(args[0]) + refuseNumberedParam(name) i := b.slotOf(name) if i < 0 { raise("NameError", "local variable '%s' is not defined for %s", name, b.ToS()) @@ -302,6 +303,7 @@ func (vm *VM) registerBinding() { cBinding.define("local_variable_set", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value { b := self.(*Binding) name := vm.bindingVarName(args[0]) + refuseNumberedParam(name) if i := b.slotOf(name); i >= 0 { // The write lands where the name LIVES, which for a binding taken inside a // block is an ANCESTOR env. Writing b.env.slots[i] instead put the value in @@ -318,7 +320,12 @@ func (vm *VM) registerBinding() { return args[1] }) cBinding.define("local_variable_defined?", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value { - return object.Bool(self.(*Binding).slotOf(vm.bindingVarName(args[0])) >= 0) + name := vm.bindingVarName(args[0]) + // ⛔ RAISES, it does not answer false. MRI puts the refusal before the + // lookup (proc.c v4.0.9:649), so this is not "no such local" — it is + // "that is not the kind of question this method answers". + refuseNumberedParam(name) + return object.Bool(self.(*Binding).slotOf(name) >= 0) }) // Ruby 4.0's implicit-parameter trio (proc.c v4.0.9:680-783). They are the @@ -388,15 +395,41 @@ func copyImplicit(m map[string]int) map[string]int { return out } -// isImplicitParamName is MRI's rb_implicit_param_p: `it`, or `_` followed by a -// single digit 1..9. `_0` and `_10` are ordinary locals. +// isImplicitParamName is MRI's rb_implicit_param_p (proc.c v4.0.9:517-521): +// `it`, or `_` followed by a single digit 1..9. `_0` and `_10` are ordinary +// locals. func isImplicitParamName(name string) bool { - if name == "it" { - return true - } + return name == "it" || isNumberedParamName(name) +} + +// isNumberedParamName is MRI's rb_numparam_id_p (proc.c v4.0.9:511-515), which +// tests `_1`..`_9` and NOT `it`. +// +// ⛔ THE TWO PREDICATES ARE NOT INTERCHANGEABLE, and MRI uses this narrower one +// in all three of Binding's local-variable methods (proc.c:549, 602, 649) while +// rb_implicit_param_p is used elsewhere. The reason is not a detail: `it` is a +// LEGAL local variable name in Ruby — `it = 5` compiles — so +// `binding.local_variable_get(:it)` must answer normally, while `_1` can never +// be a local and is refused. Widening the refusal to rb_implicit_param_p would +// break every binding that happens to hold a variable called `it`. +func isNumberedParamName(name string) bool { return len(name) == 2 && name[0] == '_' && name[1] >= '1' && name[1] <= '9' } +// refuseNumberedParam is the guard MRI puts at the top of #local_variable_get, +// #local_variable_set and #local_variable_defined? (proc.c v4.0.9:549, 602, +// 649), from Ruby 4.0. +// +// ⛔ IT TESTS THE NAME, NOT THE BINDING, and fires BEFORE any lookup — so +// `binding.local_variable_get(:_1)` raises this even in a binding that has no +// numbered parameter at all, and even at the top level. `_1` is not a local +// variable that happens to be hidden; it is not a local variable name. +func refuseNumberedParam(name string) { + if isNumberedParamName(name) { + raise("NameError", "numbered parameter '%s' is not a local variable", name) + } +} + // implicitParameterNames is bind_implicit_parameters (proc.c v4.0.9:680): [:it] // when `it` is this scope's implicit parameter, otherwise the numbered ones in // order. A block has one kind or the other and never both — `proc { |x| _1 }` is diff --git a/internal/vm/binding_implicit_params_test.go b/internal/vm/binding_implicit_params_test.go index 1a2ad397..55073334 100644 --- a/internal/vm/binding_implicit_params_test.go +++ b/internal/vm/binding_implicit_params_test.go @@ -227,9 +227,18 @@ func TestImplicitParametersAreNotLocalVariables(t *testing.T) { `rescue NameError => e; r = e.message; end }; r`, want: `local variable 'it' is not defined for #`, }, { - name: "while _1 stays visible at the version rbgo reports", + name: "and _1 is hidden the same way from 4.0", src: `r = nil; [1].each { _1; r = binding.local_variables.include?(:_1) }; r.inspect`, - want: `true`, + want: `false`, + }, { + // ⛔ But NOT the same way through local_variable_get. `it` is a legal + // local variable name, so asking for it gets the ordinary "not defined" + // above; `_1` can never be one, and MRI refuses it by name before any + // lookup (proc.c v4.0.9:549). Two different messages, deliberately. + name: "though with a different refusal, because _1 is not a NAME a local can have", + src: `r = nil; [1].each { _1; begin; binding.local_variable_get(:_1); ` + + `rescue NameError => e; r = e.message; end }; r`, + want: `numbered parameter '_1' is not a local variable`, }} { t.Run(tc.name, func(t *testing.T) { if got := evalString(t, tc.src); got != tc.want { diff --git a/internal/vm/binding_numbered_params_test.go b/internal/vm/binding_numbered_params_test.go new file mode 100644 index 00000000..598b5bae --- /dev/null +++ b/internal/vm/binding_numbered_params_test.go @@ -0,0 +1,165 @@ +package vm + +import "testing" + +// Ruby 4.0 takes the numbered parameters out of Binding's local-variable API +// (language/numbered_parameters_spec.rb:112-135, opened by `ruby_version_is +// "4.0"`; the block at :93-110 asserts the opposite below it and closes). +// +// ⛔ THE REFUSAL IS ABOUT THE NAME, NOT THE BINDING. MRI tests +// rb_numparam_id_p(lid) at the TOP of each of the three methods, before any +// lookup happens (proc.c v4.0.9:549, 602, 649), so `_1` raises in a binding +// that has no numbered parameter at all. `_1` is not a local variable that is +// being hidden; it is not a local variable name. +func TestNumberedParametersAreNotLocalVariables(t *testing.T) { + const msg = "numbered parameter '_1' is not a local variable" + for _, tc := range []struct{ name, src, want string }{ + { + "local_variable_get raises", + `begin; -> { _1; binding.local_variable_get(:_1) }.call("a"); rescue NameError => e; e.message; end`, + msg, + }, + { + "local_variable_set raises", + `begin; -> { _1; binding.local_variable_set(:_1, "b") }.call("a"); rescue NameError => e; e.message; end`, + msg, + }, + { + // RAISES rather than answering false, which is the easy one to get + // wrong: "not defined" is the natural reading and the wrong one. + "local_variable_defined? raises rather than answering false", + `begin; -> { _1; binding.local_variable_defined?(:_1) }.call("a"); rescue NameError => e; e.message; end`, + msg, + }, + + // ⛔ Before the lookup: no numbered parameter is in scope anywhere here, + // and it still raises. A guard placed after the lookup would answer + // "local variable '_1' is not defined for ..." instead. + { + "it raises even where no numbered parameter exists", + `begin; binding.local_variable_get(:_1); rescue NameError => e; e.message; end`, + msg, + }, + { + "and for defined?, where false would otherwise be the answer", + `begin; binding.local_variable_defined?(:_9); rescue NameError => e; e.message; end`, + "numbered parameter '_9' is not a local variable", + }, + + // ⛔ CONTROL: `it` is a LEGAL local variable name, so the refusal must + // NOT widen to rb_implicit_param_p. MRI uses the narrower + // rb_numparam_id_p in all three methods for exactly this reason. + { + "it is an ordinary local variable name and is not refused", + `it = 5; binding.local_variable_get(:it).inspect`, + "5", + }, + { + "and can be set through a binding", + `it = 5; binding.local_variable_set(:it, 6); it.inspect`, + "6", + }, + { + "and answers defined?", + `it = 5; binding.local_variable_defined?(:it).inspect`, + "true", + }, + + // ⛔ CONTROL: the boundary of the name shape. `_0` and `_10` are + // ordinary locals in MRI, so a refusal that tested "starts with _" or + // "_ then digits" would reject names the program may legitimately use. + { + "_0 is an ordinary local", + `_0 = 1; binding.local_variable_defined?(:_0).inspect`, + "true", + }, + { + "_10 is an ordinary local", + `_10 = 1; binding.local_variable_defined?(:_10).inspect`, + "true", + }, + { + "a plain underscore is an ordinary local", + `_ = 1; binding.local_variable_defined?(:_).inspect`, + "true", + }, + + // ⛔ CONTROL, and the trade this change must NOT make: #parameters is + // ungated (numbered_parameters_spec.rb:85) and still names `_1`. Only + // the LOCAL TABLE loses the name, never the parameter list -- which is + // why the compiler blanks b.locals and leaves b.params alone. + { + "a proc still names its numbered parameters", + `proc { _1 }.parameters.inspect`, + "[[:opt, :_1]]", + }, + { + "and a lambda does too", + `-> { _2 }.parameters.inspect`, + "[[:req, :_1], [:req, :_2]]", + }, + + // ⛔ CONTROL: they did not vanish, they moved. 4.0 gives them an API of + // their own, and taking them out of one must not take them out of both. + { + "implicit_parameters still sees them", + `-> { _1; binding.implicit_parameters }.call("a").inspect`, + "[:_1]", + }, + { + "and implicit_parameter_get still reads them", + `-> { _1; binding.implicit_parameter_get(:_1) }.call("a").inspect`, + `"a"`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if got := evalString(t, tc.src); got != tc.want { + t.Errorf("%s\n got %q\nwant %q", tc.src, got, tc.want) + } + }) + } +} + +// #local_variables is tested through runSrc rather than evalString because +// evalString wraps the source in `__v = begin … end`, and that assignment makes +// `__v` a local of the ENCLOSING scope — which a binding taken inside a block +// lists as well. The first run of the cases above read `[:__v]` where it wanted +// `[]`: the numbered parameter was already gone and the harness was supplying +// the remaining entry. An assertion written against that output would have +// pinned my wrapper's name as Ruby behaviour. +func TestNumberedParametersAreNotInLocalVariables(t *testing.T) { + for _, tc := range []struct{ name, src, want string }{ + // The two examples numbered_parameters_spec.rb:113-116 opens at 4.0. + { + "one numbered parameter", + `p(-> { _1; binding.local_variables }.call("a"))`, + "[]", + }, + { + "several", + `p(-> { _2; binding.local_variables }.call("a", "b"))`, + "[]", + }, + // ⛔ CONTROL: the list is not simply empty for every block. A real local + // still appears, so the two cases above measure the hiding and not a + // #local_variables that stopped working. + { + "a real local in the same shape still appears", + `p(-> { x = 1; binding.local_variables }.call)`, + "[:x]", + }, + // ⛔ CONTROL: `it` is blanked from the local table the same way, and + // this is the 4.0 behaviour rbgo already had before the version moved. + { + "it is not listed either", + `p(-> { it; binding.local_variables }.call("a"))`, + "[]", + }, + } { + t.Run(tc.name, func(t *testing.T) { + if got := runSrc(t, tc.src); got != tc.want { + t.Errorf("%s\n got %q\nwant %q", tc.src, got, tc.want) + } + }) + } +} diff --git a/internal/vm/kernel_introspect.go b/internal/vm/kernel_introspect.go index 11df7471..03ec73ef 100644 --- a/internal/vm/kernel_introspect.go +++ b/internal/vm/kernel_introspect.go @@ -21,7 +21,7 @@ import ( // function-local, which is how a CLI ends up printing a version the running VM // does not report. const ( - rubyVersion = "3.4.1" + rubyVersion = "4.0.9" releaseDate = "2024-12-25" revision = "48d4efcb85000e1ebae42004e963b5d0cedddcf2" ) diff --git a/internal/vm/puppet_boot_modified_test.go b/internal/vm/puppet_boot_modified_test.go index dcd5a168..cb3b4c1b 100644 --- a/internal/vm/puppet_boot_modified_test.go +++ b/internal/vm/puppet_boot_modified_test.go @@ -271,7 +271,13 @@ puts RbConfig::CONFIG["RUBY_INSTALL_NAME"] puts RbConfig::CONFIG["bindir"] puts RbConfig::CONFIG["ruby_version"] puts RbConfig::CONFIG["rubylibdir"]` - want := "ruby\nruby\n/usr/bin\n3.4.1\n/usr/lib/ruby" + // ⛔ "ruby_version" here is rbgo's FULL version string, and in MRI that key + // may hold the API version instead ("4.0.0" for every 4.0.x), since it + // names a library directory shared across teeny releases. Filed as #878 + // rather than guessed at: nothing in ruby/spec asserts this key's value, so + // the corpus cannot settle it, and a version bump is not the place to + // change behaviour on a hunch. + want := "ruby\nruby\n/usr/bin\n4.0.9\n/usr/lib/ruby" if got := runSrc(t, src); got != want { t.Errorf("RbConfig keys => %q, want %q", got, want) } diff --git a/internal/vm/wave29_kwargs_test.go b/internal/vm/wave29_kwargs_test.go index b7e5756f..e678817e 100644 --- a/internal/vm/wave29_kwargs_test.go +++ b/internal/vm/wave29_kwargs_test.go @@ -275,10 +275,11 @@ func TestImplicitItIsInvisible(t *testing.T) { {"defined?(it) is still resolved lexically", `p(-> { it; defined?(it) }.call(1))`, `"local-variable"`}, {"it still binds the argument", `p(-> { it }.call(7))`, "7"}, {"it affects arity", "p(-> { it }.arity)", "1"}, - // The numbered parameters are NOT hidden: they are named in #parameters, - // and for the Ruby version rbgo reports they are visible to a Binding. + // ⛔ The numbered parameters are hidden from ONE table of the two. They + // keep their names in #parameters, which carries no version guard, and + // leave the local table from 4.0 -- unlike `it`, which leaves both. {"_1 keeps its name", "p(proc { _1 }.parameters)", "[[:opt, :_1]]"}, - {"_1 is a binding local", `p(-> { _1; binding.local_variables }.call("a"))`, "[:_1]"}, + {"_1 is not a binding local", `p(-> { _1; binding.local_variables }.call("a"))`, "[]"}, } { if got := runSrc(t, tc.src); got != tc.want { t.Errorf("%s:\ngot %q\nwant %q", tc.name, got, tc.want) diff --git a/scripts/conformance/rubyspec/BASELINE b/scripts/conformance/rubyspec/BASELINE index f8dbcdcd..56ea62d7 100644 --- a/scripts/conformance/rubyspec/BASELINE +++ b/scripts/conformance/rubyspec/BASELINE @@ -1,87 +1,18 @@ # ruby/spec per-file baseline. Regenerate with UPDATE_BASELINE=1 scripts/conformance/rubyspec/run.sh. # One record per spec file: OKpathpassing, or FILEFAILpath. # Judged per file, so no margin is needed and every move is attributable. -# -# Recorded 2026-09-28 on main @862a9f3, corpus SPEC_SHA -# 87b1631992bd00cf0c4934474766d54dad088191, 2206 language/+core/ *_spec.rb. -# -# Updated 2026-10-08 for the frozen-def fix (#800), 23483 -> 23512 over the 20 -# files the ratchet reported improved. The twenty deltas were taken from the -# CI run on 496c39d (ubuntu-latest, TIMEOUT=60) and applied to this file, -# rather than re-measured here: this machine was carrying a desktop load of 12 -# and a pass/fail count is corrupted under load through the per-file timeout -- -# which is what "record it on an unloaded machine" above is about, and the -# reason the CI job raises TIMEOUT. Three checks before writing: the pre-edit -# sum matched the 23483 the README claimed, every one of CI's "baseline" -# columns matched the value recorded here, and the post-edit sum equals the -# 23512 CI reported. CI's verdict listed improved (20) and nothing else -- no -# regression, no new file, no change of loading -- so the deltas are the whole -# move. -# -# ⛔ 2026-10-09: ONE of those twenty was wrong to apply, and this file said so. -# core/random/bytes_spec.rb went 8 -> 9, and it is the KNOWN FLAKE held at its -# observed minimum (see "One file is recorded at its OBSERVED MINIMUM" below, -# and the two earlier waves that applied deltas and deliberately left it alone). -# The next run read 8 and the lane went red, which is the coin toss that note -# predicts in those words. Put back to 8. -# -# All three checks that commit lists were ARITHMETIC -- sums and columns -- and -# every one of them passed. None of them asked whether a number SHOULD move, -# which is the question this file answers in its own header, three times over, -# about this exact path. Measured here afterwards, same binary, six runs: -# 9 9 9 9 8 9. -# -# So: when applying a ratchet delta, read the header for the FILE being moved -# before moving it. The sum being right is not evidence that the move was. -# -# THE FRAME MATTERS, twice over, and both are why an earlier attempt at this -# file was wrong: -# -# 1. PATH. Seven path-identity specs answer differently depending on whether -# the corpus is reached through a symlink -- /tmp is a symlink to -# private/tmp on macOS, and run.sh's default CACHE lives there. Same inode, -# different spelling, 20 examples. This was swept from a REAL path. The -# underlying divergence from MRI is issue #741. -# 2. PLATFORM. This lane runs on ubuntu. FIVE files were measured on darwin and -# are recorded at CI's own LOWER ubuntu value instead: file/stat/inspect, -# kernel/backtick, marshal/load, process/setrlimit, kernel/require. Each was -# found by CI NAMING it, which is what judging per file buys -- a scalar -# floor would have reported one number and left the reader to guess which of -# 2206 files moved. Under-recording is the safe direction: a file that -# passes more is reported as improved, never failed. The baseline belongs to -# the lane that enforces it. -# -# Swept twice; the two sweeps were byte-identical across all 2206 records. -# -# 2026-09-29, #741 (ISeq path vs realpath): THREE files raised by applying the -# darwin-measured DELTA to the recorded value, not by overwriting with a darwin -# sweep -- core/kernel/require_spec.rb is one of the five held at CI's lower -# ubuntu number, and an overwrite would have lost that. The deltas, from two -# sweeps of the same real-path corpus before and after the change: -# core/thread/backtrace/location/absolute_path_spec.rb +3 (2 -> 5) -# core/kernel/require_relative_spec.rb +1 (54 -> 55) -# core/kernel/require_spec.rb +1 (68 -> 69, from 69 -> 70) -# core/random/bytes_spec.rb read 9 in both post-change sweeps and is deliberately -# left at 8: it is the known flake named below, not a mover. -# -# One file is recorded at its OBSERVED MINIMUM rather than its usual value: -# core/random/bytes_spec.rb answers 8 or 9 on the same binary, on both engines, -# and every wave measuring conformance today named it. Recorded at 8, so a run -# that reads 9 reports an improvement and one that reads 8 passes. Recording a -# flake at its high value would fail the lane on a coin toss -- two sweeps here -# read 9 and a third, under heavier load, read 8. -# -# 2026-09-30, frozen_string_literal magic comment (+ parser v0.10.0's unary -# plus): FIVE records raised by APPLYING the measured delta, not by overwriting -# from a darwin sweep -- an overwrite would discard the five values held at CI's -# lower ubuntu number and the flake note below. The deltas are the difference -# between two sweeps of the same real-path corpus run back to back, one with -# origin/main's binary and one with the branch's: -# language/magic_comment_spec.rb +5 core/kernel/eval_spec.rb +2 -# core/proc/call_spec.rb +1 core/string/uplus_spec.rb +1 -# language/encoding_spec.rb +1 -# core/random/bytes_spec.rb read 9 on BOTH binaries and stays at 8: the known -# flake named below, not a mover. +# ⛔ ABSORBED 11 file(s) that went backwards, recorded with -absorb on 2026-11-03: +# core/gc/config_spec.rb: 7 passing, baseline 8 (-1) +# core/objectspace/_id2ref_spec.rb: 0 passing, baseline 10 (-10) +# core/range/max_spec.rb: 30 passing, baseline 32 (-2) +# core/set/divide_spec.rb: 8 passing, baseline 9 (-1) +# core/set/equal_value_spec.rb: 3 passing, baseline 4 (-1) +# core/set/exclusion_spec.rb: 2 passing, baseline 3 (-1) +# core/set/flatten_merge_spec.rb: 0 passing, baseline 3 (-3) +# core/set/flatten_spec.rb: 9 passing, baseline 10 (-1) +# core/set/join_spec.rb: 4 passing, baseline 5 (-1) +# core/set/proper_superset_spec.rb: 2 passing, baseline 3 (-1) +# core/set/superset_spec.rb: 2 passing, baseline 3 (-1) OK core/argf/argf_spec.rb 2 OK core/argf/argv_spec.rb 1 OK core/argf/binmode_spec.rb 2 @@ -140,7 +71,7 @@ OK core/array/deconstruct_spec.rb 1 OK core/array/delete_at_spec.rb 6 OK core/array/delete_if_spec.rb 13 OK core/array/delete_spec.rb 7 -OK core/array/detect_spec.rb 0 +OK core/array/detect_spec.rb 1 OK core/array/difference_spec.rb 14 OK core/array/dig_spec.rb 8 OK core/array/drop_spec.rb 11 @@ -158,7 +89,7 @@ OK core/array/fetch_values_spec.rb 8 OK core/array/fill_spec.rb 47 OK core/array/filter_spec.rb 2 OK core/array/find_index_spec.rb 8 -OK core/array/find_spec.rb 0 +OK core/array/find_spec.rb 16 OK core/array/first_spec.rb 15 OK core/array/flatten_spec.rb 32 OK core/array/frozen_spec.rb 2 @@ -224,7 +155,7 @@ OK core/array/repeated_permutation_spec.rb 11 OK core/array/replace_spec.rb 7 OK core/array/reverse_each_spec.rb 8 OK core/array/reverse_spec.rb 6 -OK core/array/rfind_spec.rb 0 +OK core/array/rfind_spec.rb 16 OK core/array/rindex_spec.rb 13 OK core/array/rotate_spec.rb 14 OK core/array/sample_spec.rb 24 @@ -266,16 +197,16 @@ OK core/basicobject/singleton_method_undefined_spec.rb 2 OK core/binding/clone_spec.rb 2 OK core/binding/dup_spec.rb 3 OK core/binding/eval_spec.rb 15 -OK core/binding/implicit_parameter_defined_spec.rb 0 -OK core/binding/implicit_parameter_get_spec.rb 0 -OK core/binding/implicit_parameters_spec.rb 0 +OK core/binding/implicit_parameter_defined_spec.rb 13 +OK core/binding/implicit_parameter_get_spec.rb 14 +OK core/binding/implicit_parameters_spec.rb 10 OK core/binding/local_variable_defined_spec.rb 8 OK core/binding/local_variable_get_spec.rb 8 OK core/binding/local_variable_set_spec.rb 7 OK core/binding/local_variables_spec.rb 5 OK core/binding/receiver_spec.rb 1 OK core/binding/source_location_spec.rb 2 -OK core/builtin_constants/builtin_constants_spec.rb 17 +OK core/builtin_constants/builtin_constants_spec.rb 27 OK core/class/allocate_spec.rb 3 OK core/class/attached_object_spec.rb 0 OK core/class/dup_spec.rb 6 @@ -339,13 +270,13 @@ OK core/conditionvariable/marshal_dump_spec.rb 1 OK core/conditionvariable/signal_spec.rb 2 OK core/conditionvariable/wait_spec.rb 6 OK core/data/constants_spec.rb 2 -OK core/data/deconstruct_keys_spec.rb 8 +OK core/data/deconstruct_keys_spec.rb 12 OK core/data/deconstruct_spec.rb 1 OK core/data/define_spec.rb 5 OK core/data/eql_spec.rb 8 OK core/data/equal_value_spec.rb 8 OK core/data/hash_spec.rb 3 -OK core/data/initialize_spec.rb 20 +OK core/data/initialize_spec.rb 24 OK core/data/inspect_spec.rb 1 OK core/data/members_spec.rb 3 OK core/data/to_h_spec.rb 7 @@ -552,7 +483,7 @@ OK core/enumerator/next_values_spec.rb 8 OK core/enumerator/peek_spec.rb 5 OK core/enumerator/peek_values_spec.rb 8 OK core/enumerator/plus_spec.rb 2 -OK core/enumerator/produce_spec.rb 5 +OK core/enumerator/produce_spec.rb 9 OK core/enumerator/product/each_spec.rb 8 OK core/enumerator/product/initialize_copy_spec.rb 6 OK core/enumerator/product/initialize_spec.rb 5 @@ -619,7 +550,7 @@ OK core/exception/equal_value_spec.rb 10 OK core/exception/errno_spec.rb 8 OK core/exception/exception_spec.rb 10 OK core/exception/exit_value_spec.rb 1 -OK core/exception/frozen_error_spec.rb 1 +OK core/exception/frozen_error_spec.rb 2 OK core/exception/full_message_spec.rb 8 OK core/exception/hierarchy_spec.rb 0 OK core/exception/inspect_spec.rb 5 @@ -663,7 +594,7 @@ OK core/fiber/current_spec.rb 1 OK core/fiber/inspect_spec.rb 6 OK core/fiber/kill_spec.rb 6 OK core/fiber/new_spec.rb 4 -OK core/fiber/raise_spec.rb 35 +OK core/fiber/raise_spec.rb 54 OK core/fiber/resume_spec.rb 15 OK core/fiber/scheduler_spec.rb 0 OK core/fiber/set_scheduler_spec.rb 5 @@ -858,7 +789,7 @@ OK core/float/uminus_spec.rb 5 OK core/float/uplus_spec.rb 1 OK core/float/zero_spec.rb 1 OK core/gc/auto_compact_spec.rb 1 -OK core/gc/config_spec.rb 8 +OK core/gc/config_spec.rb 7 OK core/gc/count_spec.rb 2 OK core/gc/disable_spec.rb 1 OK core/gc/enable_spec.rb 1 @@ -1016,7 +947,7 @@ OK core/integer/zero_spec.rb 2 OK core/io/advise_spec.rb 13 OK core/io/autoclose_spec.rb 8 OK core/io/binmode_spec.rb 7 -OK core/io/binread_spec.rb 7 +OK core/io/binread_spec.rb 8 OK core/io/binwrite_spec.rb 11 OK core/io/buffer/and_spec.rb 8 OK core/io/buffer/bit_count_spec.rb 0 @@ -1027,7 +958,7 @@ OK core/io/buffer/free_spec.rb 10 OK core/io/buffer/initialize_spec.rb 13 OK core/io/buffer/internal_spec.rb 3 OK core/io/buffer/locked_spec.rb 7 -OK core/io/buffer/map_spec.rb 15 +OK core/io/buffer/map_spec.rb 20 OK core/io/buffer/mapped_spec.rb 3 OK core/io/buffer/not_spec.rb 4 OK core/io/buffer/null_spec.rb 13 @@ -1060,7 +991,7 @@ OK core/io/fdatasync_spec.rb 0 OK core/io/fileno_spec.rb 2 OK core/io/flush_spec.rb 1 OK core/io/for_fd_spec.rb 56 -OK core/io/foreach_spec.rb 34 +OK core/io/foreach_spec.rb 35 OK core/io/fsync_spec.rb 2 OK core/io/getbyte_spec.rb 5 OK core/io/getc_spec.rb 4 @@ -1088,16 +1019,16 @@ OK core/io/putc_spec.rb 9 OK core/io/puts_spec.rb 16 OK core/io/pwrite_spec.rb 9 OK core/io/read_nonblock_spec.rb 17 -OK core/io/read_spec.rb 104 +OK core/io/read_spec.rb 105 OK core/io/readbyte_spec.rb 2 OK core/io/readchar_spec.rb 14 OK core/io/readline_spec.rb 11 -OK core/io/readlines_spec.rb 54 +OK core/io/readlines_spec.rb 55 OK core/io/readpartial_spec.rb 14 OK core/io/reopen_spec.rb 27 OK core/io/rewind_spec.rb 6 OK core/io/seek_spec.rb 15 -OK core/io/select_spec.rb 14 +OK core/io/select_spec.rb 15 OK core/io/set_encoding_by_bom_spec.rb 16 OK core/io/set_encoding_spec.rb 37 OK core/io/stat_spec.rb 1 @@ -1115,10 +1046,10 @@ OK core/io/tty_spec.rb 2 OK core/io/ungetbyte_spec.rb 6 OK core/io/ungetc_spec.rb 11 OK core/io/write_nonblock_spec.rb 18 -OK core/io/write_spec.rb 43 +OK core/io/write_spec.rb 44 OK core/kernel/Array_spec.rb 14 OK core/kernel/Complex_spec.rb 68 -OK core/kernel/Float_spec.rb 97 +OK core/kernel/Float_spec.rb 98 OK core/kernel/Hash_spec.rb 10 OK core/kernel/Integer_spec.rb 243 OK core/kernel/Rational_spec.rb 32 @@ -1163,7 +1094,7 @@ OK core/kernel/gsub_spec.rb 0 OK core/kernel/initialize_clone_spec.rb 4 OK core/kernel/initialize_copy_spec.rb 4 OK core/kernel/initialize_dup_spec.rb 3 -OK core/kernel/inspect_spec.rb 4 +OK core/kernel/inspect_spec.rb 6 OK core/kernel/instance_of_spec.rb 7 OK core/kernel/instance_variable_defined_spec.rb 6 OK core/kernel/instance_variable_get_spec.rb 17 @@ -1182,7 +1113,7 @@ OK core/kernel/methods_spec.rb 18 OK core/kernel/nil_spec.rb 1 OK core/kernel/not_match_spec.rb 3 OK core/kernel/object_id_spec.rb 13 -OK core/kernel/open_spec.rb 14 +OK core/kernel/open_spec.rb 15 OK core/kernel/p_spec.rb 8 OK core/kernel/pp_spec.rb 0 OK core/kernel/print_spec.rb 4 @@ -1291,14 +1222,14 @@ OK core/math/cosh_spec.rb 7 OK core/math/erf_spec.rb 7 OK core/math/erfc_spec.rb 7 OK core/math/exp_spec.rb 7 -OK core/math/expm1_spec.rb 0 +OK core/math/expm1_spec.rb 7 OK core/math/frexp_spec.rb 6 OK core/math/gamma_spec.rb 13 OK core/math/hypot_spec.rb 7 OK core/math/ldexp_spec.rb 12 OK core/math/lgamma_spec.rb 10 OK core/math/log10_spec.rb 9 -OK core/math/log1p_spec.rb 0 +OK core/math/log1p_spec.rb 10 OK core/math/log2_spec.rb 8 OK core/math/log_spec.rb 11 OK core/math/sin_spec.rb 7 @@ -1354,7 +1285,7 @@ OK core/module/const_get_spec.rb 45 OK core/module/const_missing_spec.rb 5 OK core/module/const_set_spec.rb 15 OK core/module/const_source_location_spec.rb 40 -OK core/module/constants_spec.rb 7 +OK core/module/constants_spec.rb 11 OK core/module/define_method_spec.rb 85 OK core/module/define_singleton_method_spec.rb 1 OK core/module/deprecate_constant_spec.rb 6 @@ -1408,7 +1339,7 @@ OK core/module/refinements_spec.rb 3 OK core/module/remove_class_variable_spec.rb 8 OK core/module/remove_const_spec.rb 11 OK core/module/remove_method_spec.rb 14 -OK core/module/ruby2_keywords_spec.rb 14 +OK core/module/ruby2_keywords_spec.rb 15 OK core/module/set_temporary_name_spec.rb 13 OK core/module/singleton_class_spec.rb 5 OK core/module/to_s_spec.rb 8 @@ -1487,7 +1418,7 @@ OK core/numeric/truncate_spec.rb 1 OK core/numeric/uminus_spec.rb 3 OK core/numeric/uplus_spec.rb 1 OK core/numeric/zero_spec.rb 2 -OK core/objectspace/_id2ref_spec.rb 10 +OK core/objectspace/_id2ref_spec.rb 0 OK core/objectspace/count_objects_spec.rb 0 OK core/objectspace/define_finalizer_spec.rb 7 OK core/objectspace/each_object_spec.rb 22 @@ -1533,8 +1464,8 @@ OK core/proc/hash_spec.rb 3 OK core/proc/inspect_spec.rb 1 OK core/proc/lambda_spec.rb 10 OK core/proc/new_spec.rb 10 -OK core/proc/parameters_spec.rb 25 -OK core/proc/ruby2_keywords_spec.rb 6 +OK core/proc/parameters_spec.rb 27 +OK core/proc/ruby2_keywords_spec.rb 7 OK core/proc/source_location_spec.rb 5 OK core/proc/to_proc_spec.rb 1 OK core/proc/to_s_spec.rb 6 @@ -1646,7 +1577,7 @@ OK core/queue/pop_spec.rb 20 OK core/queue/push_spec.rb 1 OK core/queue/shift_spec.rb 1 OK core/queue/size_spec.rb 1 -OK core/random/bytes_spec.rb 8 +OK core/random/bytes_spec.rb 9 OK core/random/default_spec.rb 1 OK core/random/equal_value_spec.rb 2 OK core/random/new_seed_spec.rb 3 @@ -1676,7 +1607,7 @@ OK core/range/include_spec.rb 78 OK core/range/initialize_spec.rb 7 OK core/range/inspect_spec.rb 4 OK core/range/last_spec.rb 13 -OK core/range/max_spec.rb 32 +OK core/range/max_spec.rb 30 OK core/range/member_spec.rb 1 OK core/range/min_spec.rb 30 OK core/range/minmax_spec.rb 13 @@ -1689,7 +1620,7 @@ OK core/range/size_spec.rb 7 OK core/range/step_spec.rb 73 OK core/range/to_a_spec.rb 7 OK core/range/to_s_spec.rb 3 -OK core/range/to_set_spec.rb 3 +OK core/range/to_set_spec.rb 4 OK core/rational/abs_spec.rb 1 OK core/rational/ceil_spec.rb 9 OK core/rational/comparison_spec.rb 13 @@ -1767,16 +1698,16 @@ OK core/set/delete_if_spec.rb 4 OK core/set/delete_spec.rb 5 OK core/set/difference_spec.rb 1 OK core/set/disjoint_spec.rb 4 -OK core/set/divide_spec.rb 9 +OK core/set/divide_spec.rb 8 OK core/set/each_spec.rb 3 OK core/set/empty_spec.rb 1 OK core/set/enumerable/to_set_spec.rb 2 OK core/set/eql_spec.rb 1 -OK core/set/equal_value_spec.rb 4 -OK core/set/exclusion_spec.rb 3 +OK core/set/equal_value_spec.rb 3 +OK core/set/exclusion_spec.rb 2 OK core/set/filter_spec.rb 1 -OK core/set/flatten_merge_spec.rb 3 -OK core/set/flatten_spec.rb 10 +OK core/set/flatten_merge_spec.rb 0 +OK core/set/flatten_spec.rb 9 OK core/set/gt_spec.rb 1 OK core/set/gte_spec.rb 1 OK core/set/hash_spec.rb 1 @@ -1786,7 +1717,7 @@ OK core/set/initialize_spec.rb 10 OK core/set/inspect_spec.rb 1 OK core/set/intersect_spec.rb 4 OK core/set/intersection_spec.rb 4 -OK core/set/join_spec.rb 5 +OK core/set/join_spec.rb 4 OK core/set/keep_if_spec.rb 4 OK core/set/length_spec.rb 1 OK core/set/lt_spec.rb 1 @@ -1798,7 +1729,7 @@ OK core/set/minus_spec.rb 3 OK core/set/plus_spec.rb 1 OK core/set/pretty_print_cycle_spec.rb 0 OK core/set/proper_subset_spec.rb 2 -OK core/set/proper_superset_spec.rb 3 +OK core/set/proper_superset_spec.rb 2 OK core/set/reject_spec.rb 5 OK core/set/replace_spec.rb 5 OK core/set/select_spec.rb 5 @@ -1807,9 +1738,9 @@ OK core/set/size_spec.rb 1 OK core/set/sortedset/sortedset_spec.rb 0 OK core/set/subset_spec.rb 2 OK core/set/subtract_spec.rb 2 -OK core/set/superset_spec.rb 3 +OK core/set/superset_spec.rb 2 OK core/set/to_a_spec.rb 1 -OK core/set/to_s_spec.rb 2 +OK core/set/to_s_spec.rb 4 OK core/set/union_spec.rb 4 OK core/signal/list_spec.rb 4 OK core/signal/signame_spec.rb 6 @@ -2070,7 +2001,7 @@ OK core/thread/new_spec.rb 6 OK core/thread/pass_spec.rb 1 OK core/thread/pending_interrupt_spec.rb 3 OK core/thread/priority_spec.rb 7 -OK core/thread/raise_spec.rb 48 +OK core/thread/raise_spec.rb 68 OK core/thread/report_on_exception_spec.rb 5 OK core/thread/run_spec.rb 3 OK core/thread/set_trace_func_spec.rb 0 @@ -2214,13 +2145,13 @@ OK language/alias_spec.rb 22 OK language/and_spec.rb 10 OK language/array_spec.rb 23 OK language/assignments_spec.rb 34 -OK language/block_spec.rb 163 +OK language/block_spec.rb 164 OK language/break_spec.rb 29 OK language/case_spec.rb 48 OK language/class_spec.rb 40 OK language/class_variable_spec.rb 13 OK language/comment_spec.rb 1 -OK language/constants_spec.rb 88 +OK language/constants_spec.rb 90 OK language/def_spec.rb 63 OK language/defined_spec.rb 256 OK language/delegation_spec.rb 12 @@ -2232,7 +2163,7 @@ OK language/for_spec.rb 32 OK language/hash_spec.rb 29 OK language/heredoc_spec.rb 10 OK language/if_spec.rb 50 -OK language/it_parameter_spec.rb 15 +OK language/it_parameter_spec.rb 16 OK language/keyword_arguments_spec.rb 20 OK language/lambda_spec.rb 56 OK language/line_spec.rb 6 @@ -2244,7 +2175,7 @@ OK language/method_spec.rb 160 OK language/module_spec.rb 14 OK language/next_spec.rb 31 OK language/not_spec.rb 9 -OK language/numbered_parameters_spec.rb 16 +OK language/numbered_parameters_spec.rb 17 OK language/numbers_spec.rb 21 OK language/optional_assignments_spec.rb 74 OK language/or_spec.rb 12 @@ -2275,7 +2206,7 @@ OK language/retry_spec.rb 3 OK language/return_spec.rb 37 OK language/safe_navigator_spec.rb 9 OK language/safe_spec.rb 1 -OK language/send_spec.rb 72 +OK language/send_spec.rb 73 OK language/singleton_class_spec.rb 47 OK language/source_encoding_spec.rb 0 OK language/string_spec.rb 25 @@ -2285,6 +2216,6 @@ OK language/throw_spec.rb 9 OK language/undef_spec.rb 6 OK language/unless_spec.rb 6 OK language/until_spec.rb 25 -OK language/variables_spec.rb 115 +OK language/variables_spec.rb 118 OK language/while_spec.rb 34 OK language/yield_spec.rb 36