Skip to content

wave 32 (lang): singleton classes of nil/true/false, post parameters, !~/!/!=, and a refinement that reaches its own blocks - #665

Merged
tannevaled merged 4 commits into
mainfrom
wave32/lang
Sep 26, 2026
Merged

tannevaled merged 4 commits into
mainfrom
wave32/lang

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

wave 32 (lang): singleton classes of nil/true/false, post parameters, !~/!/!=, and a refinement that reaches its own blocks

Four measured defects, each reproduced against MRI 4.0.5 through the same shim before it was touched, and each cited to the MRI source it was read from.

Totals, rbgo before → after, against MRI 4.0.5 run through the same shim:

tree before after Δ MRI ceiling
language/ 2314 2332 +18 2627
core/ 19908 19926 +18 —
both 22222 22258 +36 —

No file regressed in either tree, and core/'s 14 load failures are the same 14 (all pre-existing parse errors or signal/exit specs). FLOOR is untouched, as asked; it sits at 22205 and the measured total is 53 above it.

1. Singleton classes of nil / true / false, and a metaclass chain that reaches Class

MRI read: class.c v3_4_0 — special_singleton_class_of (:2199), singleton_class_of (:2224-2260), rb_singleton_class (:2299), make_metaclass (:766-792).

singleton_class_of has two arms for immediates. The first raises "can't define singleton" for T_FIXNUM, T_BIGNUM, T_FLOAT, T_SYMBOL. The second, just below, answers special_singleton_class_of for T_FALSE / T_TRUE / T_NIL: those three have no room for a per-object singleton, so Ruby hands back their class. rbgo drew no line and raised for all seven.

Every door now opens the same class — #singleton_class, class << nil, #define_singleton_method, #extend, #instance_eval — so a method defined "on" nil is an ordinary instance method of NilClass, as in MRI.

Separately, make_metaclass ends the metaclass chain at Class: RCLASS_SET_SUPER(metaclass, super ? ENSURE_EIGENCLASS(super) : rb_cClass). BasicObject is the only class with no superclass, so one missing link left every singleton class of a class cut off from Class — not in #ancestors, no #method_defined?, no #respond_to?.

file before after MRI
language/singleton_class_spec.rb 35 47 53
language/metaclass_spec.rb 18 21 24
core/kernel/singleton_class_spec.rb 4 7 10
core/module/ancestors_spec.rb 6 8 9
core/{nil,true,false}/singleton_method_spec.rb 0 each 1 each 1 each
core/basicobject/basicobject_spec.rb 6 7 13
language/variables_spec.rb 113 114 118

TestSingletonClassErrors pinned class << nil as a TypeError — it pinned the divergence. It was updated to the MRI-verified raising set (Fixnum, Symbol, Float, Bignum, all four now covered).

2. Method#arity / #parameters see post parameters, and Proc#arity sees them after a splat

MRI read: rb_iseq_min_max_arity (proc.c v3_4_0:1073-1080), rb_iseq_parameters (iseq.c v3_4_0:3578-3660), setup_parameters_complex / args_setup_post_parameters (vm_args.c v3_4_0:595, 878-892).

param.post_num is independent of has_rest: def m(a, *r, c) has post_num 1 and so does def m(a=1, b). rbgo had each half wrong in a different place, in opposite directions — which is why #650, which fixed Proc#arity for the no-splat shape, could not have found either:

  • the method half knew only the after-splat post run: def m(a=1, b) answered arity -1 for MRI's -2, and [[:opt, :a], [:opt, :b]] for MRI's [[:opt, :a], [:req, :b]];
  • the proc half read is.PostCount directly, which is 0 whenever there is a splat: proc { |a, *b, c| }.arity answered -2 for MRI's -3.

Both now read iseqPostCount, which already reported both shapes. Verified byte-for-byte against MRI across the whole matrix (def, define_method, lambda, non-lambda proc, Method#to_proc, UnboundMethod, splat and no-splat, keywords, block).

This cost a defect on the way in, and TestPostParamsBindFromTheTail is the control that caught it: making iseqRequiredPositional lead+post left exec's minReq := iseqRequiredPositional(iseq) + iseq.PostCount counting the post run twice, which rejected m(9) for def m(a=1, b). Reflective counts are worth nothing unless the binder agrees with them, so the control calls the methods.

core/method/arity_spec.rb and core/proc/arity_spec.rb stop on a shared-example the shim does not implement — under MRI too — so this one is behavioural and reflective rather than something the ratchet can show.

3. #663 — !~, ! and != are dispatched, not synthesised

MRI read: object.c v3_4_0 rb_obj_not (:264), rb_obj_not_equal (:280), rb_obj_not_match (:1678) and the three rb_define_method calls (:4265, :4266, :4427); compile.c v3_4_0's specialisation table (:4260-4297); vm_insnhelper.c v3_4_0 vm_opt_not (:7019) and vm_opt_neq (:6665).

None of the three is a compiler-synthesised negation in MRI. ! and != get specialisations (opt_not, opt_neq) that compute in place only while the receiver's method is still the built-in cfunc and otherwise fall back to the send. !~ gets no specialisation at all.

The issue asked whether one cause had produced three defects. Measured, it had produced three — each with a different shape, so fixing only the one the issue named would have left two:

  • !~ compiled to !(a =~ b): a user-defined #!~ was dead code, and so was Kernel#!~ itself. This is what inverted 11 assertions in the conformance shim.
  • unary ! was inlined unconditionally: a redefined #! never ran anywhere.
  • != dispatched, but only for a method on the class chain below BasicObject — a singleton def o.!= and a module-supplied one (o.extend M) were both invisible. Half right is the hardest kind to notice.

One resolver answers all three now (overriddenNegation, the equivalent of vm_method_cfunc_is). Kernel#!~ goes in the prelude, in Ruby, because the body is Ruby — so a redefined #=~, or a mock expecting one, is what runs, and #respond_to?, #method, #send and Kernel.instance_method(:!~) all answer as MRI's do.

internal/aot/codegen.go and level2.go had to move with the interpreter or a lowered body would answer differently for the same source; their two pinned expectations were updated. The frozen prelude was regenerated and is not byte-identical — it carries the new <module:Kernel> / !~ ISeqs, which is the check that the emitter saw the addition.

Cost, measured serially, three runs each, on a loop that does nothing but negate 10M times: 1.11s → 1.20s user, ≈ +8% of a synthetic worst case.

file before after MRI
core/kernel/not_match_spec.rb 2 (+1 error) 3 3
core/exception/full_message_spec.rb 4 8 20
language/match_spec.rb 1 2 7
language/defined_spec.rb 255 256 257

4. A refinement activated in a block body now reaches the blocks written in it

MRI read: rb_vm_cref_replace_with_duplicated_cref (vm.c v3_4_0:1931) → vm_cref_replace_with_duplicated_cref (vm_insnhelper.c v3_4_0:912), from mod_using (eval.c:1547) and top_using (eval.c:1890); vm_cref_push and pushed_by_eval; rb_yield_refine_block.

using replaces the calling frame's cref with a duplicate carrying the refinements, so every block created after it captures the replacement and MRI walks the whole chain. rbgo already performed that replacement on the frame's cref entry — which is why 1.foo written directly in a Module.new { using R; … } body worked. Everything written inside it did not: the block-literal path read the lexCref local, settled before using ran. A refinement activated in such a body reached no nested block at all — not Module.new {}, not Class.new {}, not #class_eval, not #instance_eval/#instance_exec, not even [0].each { }.

rbgo keeps one *RClass per frame, so MRI's chain is approximated by its two ends — the frame's definee (MRI's pushed link: the refinement inside a refine block, the eval target inside a class_eval) and its cref (the textual one it was pushed onto), innermost first — plus Proc.refDefinee, which carries the second end onto a block so a lambda written in a method of the class still has a link back.

Consulting only the cref cost 3 examples in refine_spec (a refine block's sibling refinements hang off the pushed link); consulting only the definee was the original defect. TestCrefChainKeepsItsOtherEnd is the control for the other thing a cref is for: a constant inside a class_eval block still resolves textually, because MRI skips a pushed_by_eval cref when resolving one.

file before after MRI
core/module/using_spec.rb 14 17 20
core/module/refine_spec.rb 29 30 38

What was left, and why

  • language/method_spec.rb stayed at 55 of 62. Reproducing it first overturned the premise: 9 of its 16 non-passes are errors MRI has too (a shared-example the shim does not implement). Of the 7 real ones, 5 are the :strict_unused_block warning category and 2 are keyword/positional Hash separation. Neither is the arity defect, and the warning category lives in files another agent owns this wave.
  • Six examples in language/singleton_class_spec.rb all need builtins.go: #is_a? resolves through classOf rather than the singleton class; Class#new / #allocate do not refuse a singleton class (and #new panics on one, recovered as an ArgumentError — worth its own issue); and a frozen object's singleton class is not frozen.
  • Object.new.method(:!~).parameters answers [[:req, :other]] where MRI answers [[:req]]: MRI's is a cfunc with no parameter name and a Ruby definition must spell one. Nothing in language/ or core/ reads it.
  • A module's metaclass superclass is still nil where MRI says Module (Module.new.singleton_class.superclass). The class root is fixed once at boot; the module case needs cModule where RClass.metaClass cannot reach it. No example in either tree asks.
  • def nil.foo does not parse — the parser refuses nil as a def receiver. That is the external parser package, not this repo.
  • core/module/using_spec.rb's remaining 3: one instance_eval shape needing a fourth chain link, and two over-activation examples that were already failing.
  • Most of refine_spec's gap is indirect dispatch (#send, #respond_to?, #method, Symbol#to_proc, #binding honouring refinements), which lives in refinement.go.
  • A pre-existing flake, not touched: TestWave28KernelSleepScheduler pins sleep(o) to "0" for a 0.001s sleep, while the neighbouring TestWave28SleepReturnsWholeSeconds documents that a sub-second sleep "can only answer 0 or 1". It failed once here and passed 5× on a rerun and 5× on unmodified main.

Files outside the wave's exclusive set

Two, both minimal and neither on the do-not-touch list: internal/vm/arith.go (one function body — hasCustomNeq lives there) and internal/vm/prelude.rb (one method appended). Fixing != and giving !~ a real method could not be done from the exclusive set alone, and leaving either half would have left the other lying.

🤖 Generated with Claude Code

tannevaled and others added 4 commits September 26, 2026 08:45
…es Class

MRI read: class.c v3_4_0 — special_singleton_class_of (:2199),
rb_special_singleton_class (:2210), singleton_class_of (:2224-2260),
rb_singleton_class (:2299) and make_metaclass (:766-792).

Two defects, one file apart, both measured through the repaired ruby/spec shim
against MRI 4.0.5 run through the same shim.

1. `nil.singleton_class` raised TypeError.

   singleton_class_of has TWO arms for immediates. The first raises "can't
   define singleton" — T_FIXNUM, T_BIGNUM, T_FLOAT, T_SYMBOL (class.c:2228).
   The second, just below it, answers special_singleton_class_of for T_FALSE /
   T_TRUE / T_NIL (class.c:2236): nil, true and false have no room for a
   per-object singleton, so Ruby hands back their CLASS — NilClass, TrueClass,
   FalseClass. rbgo drew no such line and raised for all seven.

   specialSingletonClass is that function; ensureSingleton consults it first, so
   every door into a singleton class opens the same one: #singleton_class,
   `class << nil`, #define_singleton_method, #extend and #instance_eval. A
   method defined "on" nil is therefore an ordinary instance method of NilClass,
   reached by every nil in the program, which is what MRI does.

   Note the two classes are NOT singleton classes: #singleton_class? is false
   and #superclass is Object. A fabricated singleton would have answered both
   the other way.

2. `BasicObject.singleton_class.superclass` was nil.

   make_metaclass ends the metaclass chain at Class:

       RCLASS_SET_SUPER(metaclass, super ? ENSURE_EIGENCLASS(super) : rb_cClass)
                                                            (class.c:786)

   BasicObject is the only class with no superclass, so one missing link left
   every singleton class of a class cut off from Class: #ancestors did not
   include it, its instance methods were not #method_defined?, and its class
   methods drew no #respond_to?. closeMetaclassChain wires it once at boot,
   before any metaclass is asked for, since RClass.metaClass cannot reach cClass
   from where it runs.

Measured, per file, rbgo before -> after against MRI 4.0.5 through the same
shim:

    language/singleton_class_spec.rb   35 -> 47   (MRI 53)
    language/metaclass_spec.rb         18 -> 21   (MRI 24)
    language/variables_spec.rb        113 -> 114  (MRI 118)
    language/ total                  2314 -> 2330 (MRI 2627)

No file regressed.

TestSingletonClassErrors pinned `class << nil` as a TypeError — it pinned the
divergence. Updated to the MRI-verified set (Fixnum, Symbol, Float, Bignum: the
raising arm, all four now covered) with the nil/true/false side moved to
TestSpecialSingletonClass.

Left, all of them behind builtins.go, which another agent owns this wave:
#is_a? resolves through classOf rather than the singleton class, so
`o.is_a?(o.singleton_class)` and the two "subclass of Class's singleton class"
examples still fail; Class#new / #allocate do not refuse a singleton class (and
#new panics on one, recovered as an ArgumentError); and a frozen object's
singleton class is not frozen. Six examples in singleton_class_spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hem after a splat

MRI read: rb_iseq_min_max_arity (proc.c v3_4_0:1073-1080), rb_iseq_parameters
(iseq.c v3_4_0:3578-3660) and setup_parameters_complex / args_setup_post_parameters
(vm_args.c v3_4_0:595, 878-892).

param.post_num is the count of positional parameters that bind from the TAIL,
and it is independent of has_rest: `def m(a, *r, c)` has post_num 1 and so does
`def m(a=1, b)`. Neither rb_iseq_min_max_arity's `lead_num + post_num` nor
rb_iseq_parameters' `[post_start, post_start+post_num) -> :req` consults the
splat.

rbgo had each half wrong in a DIFFERENT place, in opposite directions, which is
why #650 — which fixed Proc#arity for the no-splat shape — could not have found
either:

  * the METHOD half (iseqRequiredPositional, iseqHasOptional, buildParamsList)
    knew only the after-splat post run. `def m(a=1, b)` answered arity -1 for
    MRI's -2 and parameters [[:opt, :a], [:opt, :b]] for MRI's
    [[:opt, :a], [:req, :b]] — the post parameter reported as optional.

  * the PROC half (Proc#arityVal) read is.PostCount directly, which is 0
    whenever there IS a splat. `proc { |a, *b, c| }.arity` answered -2 for MRI's
    -3, and so did the lambda.

iseqPostCount already reported both shapes; every reader now goes through it,
and iseqPostStart names the index rb_iseq_parameters walks from. One rule, one
place: the binder in exec was already right, and reads the same pair.

That last part cost a defect on the way in. iseqRequiredPositional is now
lead+post, so exec's `minReq := iseqRequiredPositional(iseq) + iseq.PostCount`
counted the no-splat post run twice and rejected `m(9)` for `def m(a=1, b)` with
"given 1, expected 2". TestPostParamsBindFromTheTail is the control that caught
it — the reflective counts are worth nothing unless the binder agrees with them,
so it calls the methods rather than only asking their arity.

Verified byte-for-byte against MRI 4.0.5 across the arity/parameters matrix
(def, define_method, lambda, non-lambda proc, Method#to_proc, UnboundMethod,
splat and no-splat, keywords and block). language/ total unchanged at 2330:
core/method/arity_spec.rb and core/proc/arity_spec.rb both stop on a shared-
example the shim does not implement — under MRI too — so the win here is
behavioural and reflective rather than one the ratchet can show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MRI read: object.c v3_4_0 — rb_obj_not (:264), rb_obj_not_equal (:280),
rb_obj_not_match (:1678), and the three rb_define_method calls that install
them on BasicObject and Kernel (:4265, :4266, :4427); compile.c v3_4_0's
specialisation table (:4260-4297); vm_insnhelper.c v3_4_0 vm_opt_not (:7019)
and vm_opt_neq (:6665).

In MRI none of the three is a compiler-synthesised negation. All three are real
methods. `!` and `!=` get SPECIALISED instructions, opt_not and opt_neq, which
compute the answer in place only while the receiver's method is still the
built-in cfunc (vm_method_cfunc_is) and otherwise fall back to the send. `!~`
gets no specialisation at all: idNMatch is absent from the opt_* table, so
every `a !~ b` is an ordinary method call.

The issue asked whether one cause had produced three defects. Measured against
MRI 4.0.5 it had produced three, but each with a different shape — so fixing
only the one the issue named would have left two:

  * `!~` was compiled to `!(a =~ b)`. A user-defined #!~ was DEAD CODE, and so
    was Kernel#!~ itself: `Object.new.send(:!~, 1)` reported '!~' missing where
    MRI reports '=~'. This is what inverted 11 assertions in the conformance
    shim, which spells `!~` on its ShouldProxy.

  * unary `!` was inlined UNCONDITIONALLY. A redefined #! never ran, anywhere —
    not from a class, not from a singleton, not from a module.

  * `!=` dispatched, but hasCustomNeq walked vm.classOf(a).super only. A
    singleton `def o.!=` and a module-supplied one (o.extend M) were both
    invisible and the opcode inverted #== instead. Half right is the hardest
    kind to notice: the class case worked, so the mechanism looked present.

One resolver now answers all three. overriddenNegation resolves the name
through the FULL dispatch chain — singleton class and extended modules first,
then the class ancestry — and reports it only when it is not the BasicObject
default, which is vm_method_cfunc_is. notValue is opt_not; hasCustomNeq is one
line over the same resolver; the compiler emits a plain send for `!~`.

Kernel#!~ goes in the prelude, in Ruby, because the body IS Ruby: send #=~ and
negate, so a redefined #=~ or a mock expecting one is what runs. That puts the
whole reflective surface where MRI has it — #respond_to?(:!~),
Kernel.instance_method(:!~), #method(:!~), and #send.

Three call sites had to move together or the interpreter and the AOT-lowered
bodies would answer differently for the same source: internal/aot/codegen.go
and level2.go now emit vm.notValue rather than an inline negation. Their two
pinned expectations were updated to the MRI-verified form. The frozen prelude
was regenerated and is NOT byte-identical: it carries the new <module:Kernel> /
!~ ISeqs, which is the check that the emitter saw the addition.

Cost: an opt_not now consults a per-instruction inline cache (the CALL_DATA
MRI's opt_not carries) instead of negating blind. Measured serially, three runs
each, on a loop that does nothing but negate 10M times: 1.11s -> 1.20s user,
about +8% of a synthetic worst case.

Measured, per file, rbgo before -> after against MRI 4.0.5 through the same
shim:

    core/kernel/not_match_spec.rb        2 (+1 error) -> 3   (MRI 3)
    core/exception/full_message_spec.rb  4 -> 8              (MRI 20)
    language/match_spec.rb               1 -> 2              (MRI 7)
    language/defined_spec.rb           255 -> 256            (MRI 257)
    language/ total                   2330 -> 2332           (MRI 2627)

core/basicobject/not_spec.rb and not_equal_spec.rb were already at MRI's 2 and
8 and stayed there: they only exercise the defaults.

Left: Object.new.method(:!~).parameters answers [[:req, :other]] where MRI
answers [[:req]], because MRI's is a cfunc with no parameter name and a Ruby
definition must spell one. Nothing in language/ or core/ reads it.

Touches two files outside the wave's exclusive list — internal/vm/arith.go (one
function body, where hasCustomNeq lives) and internal/vm/prelude.rb (one
method appended). Neither is on the do-not-touch list; fixing `!=` and giving
`!~` a real method could not be done from the exclusive set alone, and leaving
either half would have left the other lying.

Closes #663.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in it

MRI read: rb_vm_cref_replace_with_duplicated_cref (vm.c v3_4_0:1931) →
vm_cref_replace_with_duplicated_cref (vm_insnhelper.c v3_4_0:912), called from
mod_using (eval.c v3_4_0:1547) and top_using (eval.c v3_4_0:1890); vm_cref_push
and its pushed_by_eval flag (vm_insnhelper.c); rb_yield_refine_block and
rb_mod_refine (eval.c).

`using` does not merely record a module. It REPLACES the calling frame's cref
with a duplicate that carries the refinements, so every block created after it
in that frame captures the replacement and every scope pushed inside it chains
to it. MRI then walks the whole chain in
rb_method_entry_with_refinements.

rbgo records one *RClass per frame, and adoptRefinementCref already performed
that replacement on the frame's cref ENTRY — which is why `1.foo` written
DIRECTLY in a `Module.new { using R; … }` body already worked. What did not
work was everything written inside it: the block-literal path read the lexCref
LOCAL, settled before `using` ran and frozen at where the block was written
(top level, for a Module.new body). Measured against MRI 4.0.5, a refinement
activated in such a body reached NO nested block at all — not Module.new {},
not Class.new {}, not #class_eval or #module_eval, not #instance_eval or
#instance_exec, not even `[0].each { }`.

Three links, because rbgo has no chain and MRI's has to be approximated by its
ends:

  1. frameCref() reads the frame's cref ENTRY rather than the local, so a block
     literal captures the post-`using` replacement, as MRI captures it at
     block-creation time;
  2. refinedFor consults the frame's DEFINEE and then that cref. The definee is
     MRI's pushed link — the refinement module inside a `refine` block, the
     eval target inside a class_eval — and the cref is the textual one it was
     pushed onto. Consulting only the cref cost 3 examples in refine_spec,
     because a refine block's own sibling refinements hang off the pushed link;
     consulting only the definee was the original defect. Both, innermost
     first, is what MRI's walk does.
  3. Proc.refDefinee carries the second end onto a block, because a method of a
     class whose body called `using` has the TEXTUAL scope as its frame cref —
     so a lambda written in that method had no link back at all.

refDefinee feeds refinement lookup ONLY. Constant resolution still follows cref
alone, because MRI SKIPS a pushed_by_eval cref when resolving a constant, and
TestCrefChainKeepsItsOtherEnd is the control for that: a constant inside a
class_eval block still resolves textually, Module.nesting is still the textual
nesting, and a `def` there still lands on the eval target.

Measured, per file, rbgo before -> after against MRI 4.0.5 through the same
shim:

    core/module/using_spec.rb     14 -> 17   (MRI 20)
    core/module/refine_spec.rb    29 -> 30   (MRI 38)
    core/ total                19922 -> 19926

No file regressed; language/ is unchanged at 2332 (the shapes are all in core/).

Left in using_spec: "is active for block called via instance_eval", where the
receiver is a String the method holds in an ivar — a fourth link this
two-ended approximation does not reach; and the two "is not active …"
examples, which were failing before this change and are over-activation, not
under-. Most of refine_spec's remaining gap is indirect dispatch — #send,
#respond_to?, #method, Symbol#to_proc, #binding honouring refinements — which
lives in refinement.go, owned by another agent this wave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit f0c4856 into main Sep 26, 2026
7 checks passed
@tannevaled
tannevaled deleted the wave32/lang branch September 26, 2026 08:25
tannevaled added a commit that referenced this pull request Sep 26, 2026
Three units, +283 measured, and the first wave whose numbers are trustworthy
throughout: #660 repaired the harness at the end of wave 31, so this is the
first measurement in the campaign with no known vacuous assertions under it.

  #666  core/kernel, numerics, symbols (+162). 45 of those were INVISIBLE
        rather than failing: core/integer/{pow,exponent}_spec.rb each scored 0
        because the corpus contains `100000000 ** 1000000000` and rbgo computed
        it unbounded until the harness killed the process. bignum.c's
        BIGLEN_LIMIT refuses it. core/rational and core/builtin_constants are
        now AT the ceiling.
  #668  IO/Dir encodings resolved once at open, #syswrite reaching the
        descriptor, IO::Buffer's base/size distinction (+56). THIRTEEN of the
        sixteen files it touched now sit exactly at the ceiling the harness can
        express.
  #665  singleton classes of nil/true/false, post parameters in Method#arity
        AND #parameters together, and `!~`/`!`/`!=` DISPATCHED rather than
        synthesised — one cause, three defects, found by asking whether #663's
        neighbours shared its shape.

Measured with scripts/conformance/rubyspec/run.sh against the pinned corpus at
87b1631, 16-way, TIMEOUT=25, three consecutive runs: 22489, 22489, 22488. The
floor is set thirteen below the lowest.

Three of my briefing targets dissolved under measurement again — the third wave
running. core/io/write_spec's "9 failures" is a real gap of 1 (MRI fails the
other eight through this shim); core/array/fill_spec's "9" is a gap of ZERO
(ceiling 38, rbgo 38); core/numeric/step_spec's 12.700000000000001 is what MRI
prints. Running MRI through our own shim for a per-file ceiling is what catches
these, and it is now done before planning rather than after.

The largest lever left is not in any of these lanes: core/file/printf_spec.rb
scores 0 against a ceiling of 160 because rbgo cannot parse a do...end block
inside an arrow-lambda that is a paren-less command argument. One parser fix,
160 examples — issue #669.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant