From 21afd5f1602b905aba74e12cfea8b1c3770d4449 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 21 Sep 2026 06:58:53 +0200 Subject: [PATCH] rubyspec shim: a nested describe can see the helpers of the one around it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shim runs each describe block with instance_eval, so a helper written with `def` inside the block lands on THAT context object's singleton. A nested describe runs on a different object and could not see it. core/string/valid_encoding/utf_8_spec.rb paid the whole bill: its examples call an outer `def utf8`, so all 28 raised NoMethodError. The file scored 0/28 under MRI 4.0.5 as well, run through the same shim — this was our harness, not a VM gap. With the fix both engines score 28/28. Each existing helper is forwarded explicitly to the enclosing context rather than routed through a method_missing. That distinction is not cosmetic: a method_missing on SpecContext puts a shim frame in the backtrace of a genuinely missing method, and language/send_spec asserts by name that method_missing does NOT appear there. The first attempt did exactly that and cost send_spec two examples and core/random/bytes_spec one; forwarding only methods that exist leaves a missing method raising from the caller, untouched. Measured over the whole pinned corpus with ONE binary built from main, so only the shim differs: 20905 -> 20936 passing. 28 of the 31 are utf_8_spec; the other 3 are core/module/autoload_spec, the file that crashes intermittently (#615), so they are noise rather than a second win. Refs #621. Co-Authored-By: Claude Opus 5 --- scripts/conformance/rubyspec/spec_helper.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/conformance/rubyspec/spec_helper.rb b/scripts/conformance/rubyspec/spec_helper.rb index f9bbade1..80261fd4 100644 --- a/scripts/conformance/rubyspec/spec_helper.rb +++ b/scripts/conformance/rubyspec/spec_helper.rb @@ -535,6 +535,7 @@ class SpecContext attr_reader :desc, :examples, :before_each, :after_each, :before_all def initialize(desc, parent) @desc = desc + @parent = parent @examples = [] @before_each = parent ? parent.before_each.dup : [] @after_each = parent ? parent.after_each.dup : [] @@ -556,6 +557,19 @@ def before(scope = :each, &blk) def after(scope = :each, &blk); @after_each << blk if scope == :each; end def describe(d, *a, &blk) child = SpecContext.new("#{@desc} #{d}", self) + # Carry helpers written with `def` in THIS block down to the child. The block + # is instance_eval'd, so a `def` lands on this object's singleton, and a + # nested describe runs on a different object — which is how + # core/string/valid_encoding/utf_8_spec lost all 28 of its examples: they call + # an outer `def utf8`. Forward each EXISTING helper explicitly rather than + # adding a method_missing, because a genuinely missing method must still raise + # from the caller with no shim frame in the backtrace — language/send_spec + # asserts precisely that, and a method_missing here broke it. + parent = self + sc = singleton_class + (sc.instance_methods(false) + sc.private_instance_methods(false)).each do |m| + child.define_singleton_method(m) { |*ar, &bl| parent.send(m, *ar, &bl) } + end $ctx_stack.push(child) begin child.instance_eval(&blk) if blk