Skip to content

reverse_each with a block walks a boxed user Enumerable last to first - #10177

Merged
matz merged 1 commit into
matz:masterfrom
blacktm:non-enumerable-box-each
Oct 25, 2026
Merged

matz merged 1 commit into
matz:masterfrom
blacktm:non-enumerable-box-each

Conversation

@blacktm

@blacktm blacktm commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

A user object that includes Enumerable, read back out of a container, ran
the block of reverse_each { } zero times: on master,
[Nums.new, 0][0].reverse_each { |x| print x } prints nothing where CRuby
prints 321, and a break inside it answers the object instead of the
break value. The same holds for a class whose #each forwards its block
(["C", "B", "A"] expected, [] measured). The blockless
reverse_each.to_a already answers [3, 2, 1]. This came up in our
compatibility testing against CRuby.

The boxed block walk of reverse_each (iter_ewi_zip_poly_arms,
src/codegen_iter.c) now reads the receiver's elements through
sp_poly_enum_chk, the helper the blockless form uses, before
sp_poly_iter_subject, so a user Enumerable is walked as the Array its
to_a answers (what its #each yields, unless the class has a to_a of
its own). Every other walk keeps sp_poly_iter_walk and the check line is
unchanged. A class that defines its own reverse_each keeps it; an object
with no #each still raises NoMethodError naming reverse_each.

The new test is test/poly_user_enum_reverse_each.rb; on master:

spinel diff: output-diff
  program: test/poly_user_enum_reverse_each.rb
  ruby:    exit 0
  spinel:  exit 0

--- stdout (ruby)
+++ stdout (spinel)
@@ -1,10 +1,10 @@
-321
+
 true
-20
-["C", "B", "A"]
+#<Nums:0xADDR>
+[]
 :own
 [:plain, NoMethodError, "undefined method 'reverse_each' for an instance of Plain"]
 54
-321
+
 1
-[[3, 4], [1, 2], [[3, 4]], [[1, 2]]]
+[]

make gate (on this branch merged with current master)

scale-test: boxed Hash store work at 2x the methods is 1.93x (limit 2.20)
scale-test: boxed-receiver alias work at 2x the writes is 1.85x (limit 2.20)
scale-test: instance_eval forwarding work at 2x the wrappers is 1.72x (limit 2.50)
scale-test: work at 4x the program is 4.94x (linear 4.00, limit 5.40)
scale-test: work at 4x the program, compiled to C, is 6.27x (limit 6.90)
scale-test: call-shape work at 4x the units, compiled to C, is 4.29x (linear 4.00, limit 4.50)
scale-test: the analysis of a String chain at 4x the links is 4.14x (linear 4.00, limit 4.60)
Tests:     8892 pass,        0 fail,        0 error
gate: ALL GREEN
  • New tests have .expected files that match CRuby 4.0 run with --enable-frozen-string-literal
  • Values past 2^31 are marked # spinel: int64 (not applicable: none)
  • If optcarrot's generated C changed: callgrind numbers, checksum 59662 (not applicable: its generated C is unchanged)
  • Depends on: # (none)

Validation

Branch 5f45f24e6 on 38313140d, merged for the make gate with origin/master 38313140d; expectations from CRuby 4.0.7 with --enable-frozen-string-literal. The generated C of 17 of the 8,675 suite programs and benchmarks changes: the new test; six whose boxed reverse_each block walk now reads the receiver through sp_poly_enum_chk (test/boxed_hash_each_rest_param.rb, test/boxed_rev_chain.rb, test/hash_reverse_each_answers_receiver.rb, test/poly_each_returns_receiver.rb, test/poly_iter_non_enumerable_object.rb, test/safe_nav_block_calls.rb), with the same output; and ten whose only change is the revision string RUBY_DESCRIPTION embeds or a source path. The new test passes at -O0, -O1 and -O2, and under SPINEL_GC_STRESS=1.

Left alone

  • A class with its own to_a has a boxed reverse_each { } walk what to_a answers where CRuby walks what each yields; master does the same.
  • If any class in the program defines its own reverse_each, a boxed Enumerable without one raises NoMethodError where CRuby walks it, and a class with each but no include Enumerable walks under reverse_each { } where CRuby raises NoMethodError; master does the same.
  • A boxed Dir's reverse_each { } raises NoMethodError, a boxed (1..) raises RangeError where CRuby raises TypeError, and redo in a boxed reverse_each block is refused at compile time; master does the same.
  • For a user Enumerable that yields several values per step, boxed each_with_index, map, each_with_object and inject bind them unlike CRuby (map { |x| x } answers [[1, 2], [3, 4]], CRuby [1, 3]); master does the same.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reverse_each behavior for custom Enumerable objects and string ranges. Custom objects are iterated through their array results when applicable, while objects with their own reverse_each method continue to use it.
    • Added coverage for yielded values, return values, early block exits, and multiple block arguments.

A user object that includes Enumerable, read back out of a container (a
TY_POLY receiver), ran the block of `reverse_each { }` zero times and
answered the object: the reverse walk read the length and elements of the
object itself, which has neither.

The boxed reverse walk now reads the receiver's elements first, as its
to_a answers them (sp_poly_enum_chk, in iter_ewi_zip_poly_arms,
src/codegen_iter.c), as the blockless reverse_each already does, then walks
them from the end. A
class that defines its own reverse_each keeps it; an object with no #each
still raises NoMethodError naming reverse_each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3809946d-eebf-49e6-8712-b7e2f6c30ca4
Commits

Reviewing files that changed from the base of the PR and between 2814693 and 5f45f24.

Files selected for processing: 3
  • src/codegen_iter.c
  • test/poly_user_enum_reverse_each.rb
  • test/poly_user_enum_reverse_each.rb.expected

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Walkthrough

For boxed receivers, polymorphic reverse_each now checks the receiver with sp_poly_enum_chk before obtaining its iteration subject. The added gc-stress test covers custom Enumerable fixtures and built-in values.

Changes: Polymorphic reverse_each handling

Layer / File(s) Summary
Boxed receiver iteration and coverage
src/codegen_iter.c, test/poly_user_enum_reverse_each.rb, test/poly_user_enum_reverse_each.rb.expected
The reverse_each path checks boxed receivers with sp_poly_enum_chk before obtaining the iteration subject. The gc-stress script exercises custom and built-in receivers, and the expected output records the results.

Priority: Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Possibly related PRs

  • matz/spinel#10164: Adds polymorphic block-taking Enumerable arms that obtain elements from Enumerable-including classes.
Merge Risk: Minimal · up to 5f45f

The change appears ready to merge after normal checks; no unresolved behavior issue was identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: boxed user Enumerables now support block-form reverse_each iteration from last to first.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matz
matz merged commit 6351de0 into matz:master Oct 25, 2026
5 checks passed
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.

2 participants