Skip to content

Fix the scope and argument binding of a lambda literal - #490

Merged
sinsoku merged 6 commits into
ruby:masterfrom
sinsoku:lambda-method-args-fixes
Sep 26, 2026
Merged

sinsoku merged 6 commits into
ruby:masterfrom
sinsoku:lambda-method-args-fixes

Conversation

@sinsoku

@sinsoku sinsoku commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation / Background

This Pull Request has been created because reviewing #481 after it merged turned up five defects:

def foo
  x = 1
  ->(x) { }.call("str")
  x                             #=> Integer, inferred as String
end

->(h) { h }.call(k: 1)          #=> wrong number of arguments (0 for 1)
->(&b) { b }.call { 1 }         #=> untyped, not Proc
[1, 2].map(&->(*a) { a })       #=> Array[Array[untyped]], not Array[Array[Integer]]
[1].each { |x: helper(1)| x }   #=> no diagnostic for the call in the default

Detail

One commit per defect, in the order above.

  • An empty body built its DummyNilNode with the enclosing LocalEnv, which install0 then takes as the block's own scope.
  • Keywords passed to formals that take none are the last positional hash, which only the method path did before binding.
  • Proc#call receives the block it is given, but nothing bound it to &b.
  • A lambda passed with & still went through the block-shaped binder, which knows only positionals and checks no arity.
  • Only LambdaNode listed keyword defaults as subnodes, so in a block nothing installed the nodes AST.parse_params had built.

A block still does not bind keywords; only the default expression is analyzed. lambda {}, proc {} and Proc.new {} lose Type::Proc through their RBS declarations, so none of the argument binding reaches them.

The last commit fixes nothing: it covers the block of super() { ... }, which #481 made report a return type mismatch instead of printing the node class from wrong_return_type.

Verification

rake test passes at every commit, and each added scenario, except the one for super, fails on the commit before its fix.

🤖 Generated with Claude Code

A block or lambda with an empty body built its DummyNilNode with the
enclosing LocalEnv, and install0 takes the body's LocalEnv as its own.
So `->(x) { }` bound x in the enclosing scope, and an empty lambda took
the enclosing method's return and the enclosing block's break as the
value #call returns. An empty block had the same bug before a lambda
literal was analyzed at all.
A method with no keyword parameters takes `f(k: 1)` as a trailing
positional hash, but a lambda call skipped that step and reported
`->(h) { h }.call(k: 1)` as a wrong number of arguments.
A method call binds the block it is given to its `&b` parameter, but a
lambda call left that parameter untyped. Proc#call already receives the
block, so it is bound the same way.
A lambda given with `&` to an RBS-declared method was still bound like
a block: its rest and post parameters got nothing and its arity was
not checked, while the same lambda called with #call bound fully.
parse_params builds nodes for keyword defaults, but in a block they
were never installed, so a call in `{ |x: helper(1)| }` got no
diagnostics. A block still does not bind keywords.
The block of `super() { ... }` used to fall through to a debug `pp` in
wrong_return_type and report nothing.
@sinsoku
sinsoku force-pushed the lambda-method-args-fixes branch from 14752fb to b4061a6 Compare September 26, 2026 17:25
@sinsoku
sinsoku merged commit 2e21781 into ruby:master Sep 26, 2026
6 checks passed
@sinsoku
sinsoku deleted the lambda-method-args-fixes branch September 26, 2026 17:35
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.

1 participant