Skip to content

Infer return types for block calls in returns and branches - #106

Draft
apiology wants to merge 2 commits into
masterfrom
collect-block-values-in-branches
Draft

apiology wants to merge 2 commits into
masterfrom
collect-block-values-in-branches

Conversation

@apiology

@apiology apiology commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

Claude:

Problem: A method whose return value is a call with a block gets no inferred type when that call is the value of a return or of a conditional branch, so typecheck --level strong reports "return type could not be inferred" despite accurate @return tags upstream.

# @return [Array<Integer>]
def list; [1]; end

list.map { |x| x.to_s }          # => Array<String>
return list.map { |x| x.to_s }   # => undefined
return list.map(&:to_s)          # => Array<String>

if/else, unless/else, case/when, a ternary, and an if assigned to a local behave the same way; a block-pass or a call with no block does not.

Solution: reduce_to_value_nodes, which collects values from branches and from return, kept only the explicit returns inside a block body and discarded the block itself, where from_value_position_statement already treats a block as a value. It now pushes the block node too, additively: a return inside a block returns from the enclosing method, so both count.

Two @sg-ignore markers this obsoletes are removed, one recording that FlowSensitiveTyping#find_var had no inferrable return type. One is added where the new types reach a masgn call and expose a real narrowing gap, retiring the @todo there that asked for that alert. :numblock is untouched: NodeChainer recognises only :block.

A call with a block written at the call site contributed no type when
it was the value of a `return` or of a conditional branch, so a method
whose return value takes that shape got no inferred type at all.

Two collectors disagreed about `:block`. from_value_position_statement,
which handles a method body's tail expression, treats a block as a
first class value via FUNCTION_VALUE. reduce_to_value_nodes, which
handles if/unless branches, case branches and `return`, kept only the
explicit returns found inside the block body and discarded the block
itself.

Push the block node there as well. The change is additive: a `return`
inside a block returns from the enclosing method, so the block's own
value and any inner explicit returns are both genuine possible values,
and the existing explicit_return_values_from_compound_statement call
stays alongside it.

Two @sg-ignore markers the change obsoletes are removed, including the
one recording that FlowSensitiveTyping#find_var had no inferrable
return type - its body is an if/else whose branches are block calls,
which is exactly this defect.

`:numblock` is left alone. Both collectors already push it as a value
through their fallthrough, yet it still infers as undefined, because
NodeChainer recognises only `:block` and the type appears nowhere else
in lib/. Supporting it is a separate change.
Collecting block values from branches gives `pin` a real type in
MasgnNode#process, where it was previously undefined and absorbed every
call made on it. The typechecker now reports:

  Unresolved call to mass_assignment=

`pins.find { |iv| ... && iv.is_a?(Pin::BaseVariable) }` types as
Pin::Base, since the block's is_a? guard does not narrow what find
returns, and mass_assignment is an attr_accessor on Pin::BaseVariable.
At runtime the guard does guarantee it, so the code is correct and the
gap is in the tool. Mark it with the reason string already used for
this gap elsewhere rather than coining a new one.

Remove the @todo above it asking for an alert when a non-existent
method is called there. That alert is exactly what now fires, so the
note is satisfied; leaving it would tell the next reader the check is
still missing.

Typecheck goes from 531 problems to 530 against the branch point.
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