Skip to content

analyze: convert a user &object block argument through to_proc#1817

Merged
matz merged 1 commit into
matz:masterfrom
ryanseys:rs-user-to-proc-block-arg
Jul 8, 2026
Merged

analyze: convert a user &object block argument through to_proc#1817
matz merged 1 commit into
matz:masterfrom
ryanseys:rs-user-to-proc-block-arg

Conversation

@ryanseys

@ryanseys ryanseys commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

recv.iter(&obj) where obj is a user object defining #to_proc was rejected. Ruby calls obj.to_proc exactly once to obtain the block, then runs that proc per element.

This adds an analysis pre-pass that hoists __tproc = obj.to_proc to the statement enclosing the call and rewrites the block argument to the hoisted local, so the existing value-callable desugar forwards the once-computed proc. Hoisting (rather than cloning obj into the block body) makes to_proc run a single time — observable when it has a side effect or returns a stateful proc — matching Ruby's &obj semantics. The pass declines, leaving the existing loud reject, when it cannot locate the enclosing statement.

Covered by test/user_to_proc_block_arg.rb (map / select / count iterators, an inline &Obj.new and a variable-held converter, and a side-effecting to_proc that proves single evaluation), with ruby as the oracle.

A to_proc that returns a lambda capturing self/ivars and then escapes its object is a separate, pre-existing limitation (it fails the same way for a direct pr = obj.to_proc; arr.map(&pr)), so it is out of scope here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new desugaring pass, desugar_to_proc_block_arg, which hoists the evaluation of to_proc on user objects passed as block arguments to ensure it is evaluated exactly once, matching Ruby's behavior. Feedback is provided regarding the helper function tp_enclosing_stmt, which currently has a worst-case complexity of O(N^2); an O(N) optimization using a single top-down recursive traversal is suggested to prevent potential compilation slowdowns on large ASTs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/analyze_pass.c Outdated
`recv.iter(&obj)` where obj is a user object defining `#to_proc` was
rejected. Ruby calls `obj.to_proc` exactly once to obtain the block, then
runs that proc per element.

Add an analysis pre-pass that hoists `__tproc = obj.to_proc` to the
statement enclosing the call and rewrites the block argument to the
hoisted local, so the existing value-callable desugar forwards the
once-computed proc. Hoisting (rather than cloning obj into the block)
makes to_proc run a single time -- observable when to_proc has a side
effect or returns a stateful proc -- matching Ruby's `&obj` semantics. The
pass declines, leaving the existing loud reject, when it cannot locate the
enclosing statement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ryanseys ryanseys force-pushed the rs-user-to-proc-block-arg branch from 062bfff to 6d80738 Compare July 8, 2026 07:30
@ryanseys ryanseys marked this pull request as ready for review July 8, 2026 07:30
@matz matz merged commit 6c15920 into matz:master Jul 8, 2026
3 checks passed
@ryanseys ryanseys deleted the rs-user-to-proc-block-arg branch July 8, 2026 17:16
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