Skip to content

Range#each refuses a begin it cannot step, and compares with <=> (fixes #144) - #145

Open
jiro-by-nori[bot] wants to merge 1 commit into
mainfrom
range-each-non-succ-begin
Open

jiro-by-nori[bot] wants to merge 1 commit into
mainfrom
range-each-non-succ-begin

Conversation

@jiro-by-nori

@jiro-by-nori jiro-by-nori Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #144.

What was wrong

Range#each stepped with succ and compared with </<=. CRuby's
range_each does neither, so three separate answers came out wrong:

program before CRuby 4.0.5
(1.0..2.0).each { |x| p x } prints 1.0, then NoMethodError: undefined method 'succ' for an instance of Float TypeError: can't iterate from Float, nothing printed
(1.0..2.0).map { |x| x } NoMethodError (succ on Float) TypeError: can't iterate from Float
(obj..obj).each { }, obj defining succ and <=> but no < NoMethodError: undefined method '<=' for an instance of ... iterates

NoMethodError is inside the family the validator claims to exclude, so these
are wrong answers rather than gates (the line #27 settled) — and the first one
yields 1.0 before failing, so it has already produced output.

What changed

ruby-lean/prelude/prelude.rb, Range#each:

  • Refuse a begin CRuby cannot step, before yielding. CRuby asks
    rb_respond_to(beg, succ) and raises TypeError: can't iterate from <Class>
    otherwise. respond_to? in the prelude is CRuby's name table, so a begin
    whose succ CRuby has but the model does not model (String, Symbol)
    keeps gating on the unmodeled succ instead of turning into a new wrong
    TypeError. The beginless/endless gate is unchanged: (1..).each really is
    infinite, and a faithful infinite loop is indistinguishable from a stuck
    model.
  • Compare with <=> for a non-Integer begin, tested before each step, with
    the end value yielded once after the loop.
    That is the shape CRuby's
    range_each has: an object needs succ and <=> only, and a succ that
    answers the receiver itself is yielded once and not stepped again.
  • An Integer begin keeps the native </<= loop — same answers, a step
    rather than three.

ruby-lean/RubyCore/Generated/{Prelude,PreludeJson}.lean are regenerated
(make gen, checked by make gen-check).

Test plan

New regression case difftest/corpus/regressions/range-each-non-succ-begin.rb
(sidecar status: fixed), which pins:

  • the TypeError for a Float begin on the block, map and to_a forms, and
    that nothing is yielded first;
  • that an Integer begin still iterates, including against a Float end;
  • that a class with succ and <=> only iterates (inclusive one visit,
    exclusive none, then three visits), matching CRuby byte-for-byte;
  • that a succ returning self with <=> 0 yields one element and stops.

How I validated it:

  • Reproduction first: with the sidecar at status: open and the old
    prelude, cd difftest && uv run difftest run --tier regressions --sut lean
    reported still_open: 1 with the diff
    stdout: '[TypeError, can't iterate from Float]...' vs '1.0\n[NoMethodError, ...]'
    and exception: None vs ('NoMethodError', "undefined method '<=' ...").
  • After the fix: the same run reports 223 ran, 222 agree, 1 sut_unsupported
    and held for the new case, with no other outcome changed (the one
    sut_unsupported is the pre-existing sorbet-hash-gate.rb, gated).
  • make check: ALL CHECKS PASSED — build, every proof book, the axiom
    audits, the checker corpus, difftest over bootstraptest, the regressions and
    tier-3 corpora, the unit tests and their coverage, the docs. The bootstraptest
    watermark is unchanged (ran 1287, agree 1087, sut_unsupported 194, not_compared 6, disagree 0), so no baseline moved.
  • make check-not-weakened: CHECKS NOT WEAKENED.
  • Also checked by hand against CRuby 4.0.5 that ("a".."c") and (:a..:b)
    still gate rather than raising the new TypeError, and that
    (0..10).step(2)/each_slice gates are untouched.

Note

Authored by Jiro, an autonomous agent (Nori sessions), on behalf of
@sam-xif — including the code and this description.

…(issue #144)

`Range#each` stepped with `succ` and compared with `<`/`<=`. CRuby's
`range_each` does neither. It first asks whether the begin responds to `succ`
and raises `TypeError: can't iterate from <Class>` when it does not, before
yielding anything — the model called `succ` and let that raise, so
`(1.0..2.0).each { |x| p x }` printed `1.0` and *then* answered
`NoMethodError`, and `map`/`to_a` answered `NoMethodError` where CRuby answers
`TypeError`. Those are wrong answers in the exception family the validator
claims to exclude, not gates, which is the line #27 settled.

For a begin it can step, CRuby's comparison is `<=>`, tested before each step,
with the end value yielded once after the loop. So an object that defines
`succ` and `<=>` iterates without the `<`/`<=` that `Comparable` would add, and
a `succ` that answers the receiver itself is yielded once and never stepped
again — where the old `<= 0` test answered `NoMethodError` on `<` and a `<= 0`
loop would not terminate.

An Integer begin keeps the native `<`/`<=` loop: it answers the same and costs
a step rather than three. `respond_to?` is CRuby's name table, so `("a".."c")`
and `(:a..:b)` keep gating on the unmodeled `String#succ`/`Symbol#succ` instead
of becoming a new `TypeError`, and the beginless/endless gate is unchanged.

`difftest/corpus/regressions/range-each-non-succ-begin.rb` pins all three
behaviours; it disagreed before the change and agrees after.

Validation: `make check` passes (build, every book, the axiom audits, the
checker corpus, difftest over bootstraptest at the recorded baseline, the
regressions and tier-3 tiers, the unit tests and their coverage, the docs).
`make check-not-weakened` reports CHECKS NOT WEAKENED. The bootstraptest
numbers are unchanged (ran 1287, agree 1087, sut_unsupported 194, disagree 0),
so no baseline moved.

Authored by Jiro, an autonomous agent (Nori sessions), on behalf of @sam-xif.
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.

Range#each raises NoMethodError where CRuby raises TypeError "can't iterate from <Class>", and compares with </<= where CRuby compares with <=>

1 participant