Analyze the body of a lambda literal - #481
Open
ahogappa wants to merge 4 commits into
Open
Conversation
The parsing of block parameters, the block-local scope, and the installation of the block body lived inside CallBaseNode. Nothing there depends on being a call, and a lambda literal needs exactly the same handling without being a call, so it moves into its own node that a call holds as a subnode. Block parameters now go through AST.parse_params and multi-target binding through a shared Node helper, both of which DefNode already used for the same job. Every node now answers ret_code_range, so the escape box no longer picks a code-range method by node class; a body-bearing node points at its last statement, the rest at themselves. A diagnostic on an empty block therefore points at the block instead of the whole call, and a block on `super do ... end` no longer falls through to a debug `pp`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`-> {}` was a stub that produced a bare Proc and never looked inside, so
method calls in the body got no diagnostics, classes and methods defined
there were not registered, and parameters were untyped. `lambda {}` had
none of these gaps because it goes through the block handling of a call.
LambdaNode is a BlockNode: a lambda literal builds its scope, parameters
and body exactly like a block. It is not modeled as a `lambda` call
because `->` is syntax and must not dispatch to a user-defined `lambda`
method.
Where a lambda differs from a block is how the body leaves. A block's
`return` exits the enclosing method and its `break` exits the method
that yielded; a lambda's `return` and `break` both exit the lambda, so
its body gets its own return boxes and all three escapes join the value
the caller of #call receives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A lambda literal carried a Block, which models what a yielding method hands to a block: positionals only. So a lambda whose parameters a block cannot express — rest, post, keywords — bound nothing, and the body read those parameters as nil. Its arity was not checked either, and a sole array argument was deconstructed over the parameters the way a block deconstructs one, which a lambda does not do. A lambda is entered like a method, so it now carries the formals a method carries and Proc#call binds against them. The binding itself is the one a method definition already used: pass_arguments moves off MethodDefBox onto FormalArguments, which is what both now hold. Proc#call already received the whole ActualArguments and passed on only the positionals, so the keywords and splat flags were there all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extracting the block parameters into BlockNode routed them through
parse_params, which reads the rest parameter. The previous block-only
code read just the requireds and the optionals, so it never met the node
`{ |a,| }` puts there: Prism::ImplicitRestNode, which has no #name.
A trailing comma is the only way to write a rest without naming it in a
block, and it cannot appear in a method definition or a lambda literal,
where it is a syntax error.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
A lambda literal was a blind spot:
LambdaNode#install0returned a bare Proc and never looked inside.No diagnostics in the body, no class or method defined there registered, and every
.calluntyped — which spreads through whatever the result reaches.lambda {}had none of these gaps, because it goes through the block handling of a call.Changes
CallBaseNodeintoBlockNode, which a call now holds as a subnode.LambdaNodeis aBlockNode— the same scope and parameters. It is not modeled as alambdacall, because->must not dispatch to a user-definedlambda. Itsreturnandbreakleave the lambda rather than the enclosing method, so they join the value#callreturns.Proc#callon a lambda binds arguments like a method's:pass_argumentsmoves fromMethodDefBoxontoFormalArguments, so rest, post and keyword parameters bind and arity is checked.lambda { }is left alone: which method a call reaches is known only after inference, while where an escape goes is wired when the node is built.Verification
New scenarios under
scenario/lambda/. Analyzinglib/typeprof/corewith and without the change gives byte-identical output, so the block path is untouched.🤖 Generated with Claude Code