analyze: convert a user &object block argument through to_proc#1817
Conversation
There was a problem hiding this comment.
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.
`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>
062bfff to
6d80738
Compare
recv.iter(&obj)whereobjis a user object defining#to_procwas rejected. Ruby callsobj.to_procexactly once to obtain the block, then runs that proc per element.This adds an analysis pre-pass that hoists
__tproc = obj.to_procto 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 cloningobjinto the block body) makesto_procrun a single time — observable when it has a side effect or returns a stateful proc — matching Ruby's&objsemantics. 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.newand a variable-held converter, and a side-effectingto_procthat proves single evaluation), withrubyas the oracle.A
to_procthat returns a lambda capturingself/ivars and then escapes its object is a separate, pre-existing limitation (it fails the same way for a directpr = obj.to_proc; arr.map(&pr)), so it is out of scope here.