Skip to content

Destructure block parameter groups and keep union-typed tuple elements in one position - #60

Open
apiology wants to merge 11 commits into
apiology-1196-literal-inferencefrom
destructuring-on-1223
Open

Destructure block parameter groups and keep union-typed tuple elements in one position#60
apiology wants to merge 11 commits into
apiology-1196-literal-inferencefrom
destructuring-on-1223

Conversation

@apiology

@apiology apiology commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Base: castwide#1223

Problem: A block parameter written as a destructured group gets no local pins at all, so every reference to it is reported as an unresolved call.

# @return [Hash{String, Symbol => Integer}]
def dict; { 'a' => 1, b: 2 }; end

dict.each_with_object({}) do |(key, count), memo|
  # Unresolved call to succ
  count.succ
end

# Wrong argument type ... received Array(String, nil, V)
dict.each { |key, count| count.succ }

Every block that destructures a hash entry is affected, and any hash whose keys are a union additionally reports wrong-argument errors that are not real.

Solution: Register a destructured group as one :mlhs parameter holding its signature position, type each variable inside it by projecting the group's tuple type per element index, and rebuild generic type parameters per position so a union binding one tuple slot stays one slot.

🤖 Generated with Claude Code

A destructured group (|(a, b), c|) previously produced one garbage
Parameter named with the raw sexp text and no local pins at all for the
variables inside it, so every reference to them was an unresolved call.

The group now registers as a single :mlhs Parameter (holding its
position in the block signature), and each variable inside it becomes a
local Parameter carrying an mlhs_path - the group's position plus the
element index at each nesting level - typed by projecting the group's
tuple type per position. typify_parameters also keeps its best partial
result instead of discarding everything when a single yield type (e.g.
each_with_object's unbound U generic) fails to resolve.

SKIP=Solargraph: strong self-typecheck of the touched files carries 16
problems on the parent commit already (fallout of re-enabling tuple
inference, which castwide#1223 mitigates); this diff nets
that down to 15. PoC branch - the real PR should land atop castwide#1223.
…tion

Substituting a generic type parameter with a union (e.g. Hash#each
yielding [K, V] where K is 'String, Symbol' or 'String, nil') spliced
the union's members into separate tuple positions, inflating
Array(K, V) into a 3-arity tuple. That broke block destructuring
(arity mismatch) and produced false 'Wrong argument type ... received
Array(A, B, C)' errors at strong level.

Both rebuild sites - resolve_param_generics_from_context and
UniqueType#transform - now rebuild parameters per position, wrapping
however many types a position's transformation produces back into that
single position.

Known limitation: the tag rendering is unchanged, so a multi-item
position still PRINTS ambiguously (Array(String, Symbol, Integer));
positions survive in memory but not a to_s/parse round trip.
apiology added a commit that referenced this pull request Aug 17, 2026
# Conflicts:
#	lib/solargraph/pin/parameter.rb
The partial-result fallback added in this branch returned whatever
destructure_yield_types produced even when the yielded types could not
be matched to the block's parameters. In that fallback Ruby auto-splats
a single yielded value across several parameters, but the code assigns
the WHOLE value to position 0 and leaves the rest undefined - garbage
the previous all-or-nothing gate happened to discard.

Three real downstream sites regressed from "parameters untyped" to
"parameter 0 wrongly typed as the whole value":

  hash[k] = v            # Wrong argument type: expected String,
                         #   received Array<Object, NilClass>
  proc { |exception, try, elapsed, next_int| ... }
                         # exception expected Exception, received Array

Only keep a partial result when the positions genuinely correspond -
one yielded type per parameter, or a tuple whose arity matches the
parameter count. Otherwise fall back to undefined, as before.

Four specs pin the plain multi-parameter shapes (Hash#each pair,
Array-of-tuples, 4-parameter proc, non-matching arity).
apiology added a commit that referenced this pull request Aug 17, 2026
The #60 merge (86ccceb) brought in the partial-result fallback, which
regressed three plate-spinner sites from 'parameters untyped' to
'parameter 0 typed as the whole yielded value' (jell.rb:35,
my_asana_wip.rb:292, asana_retry.rb:57). Verified against this pin: the
fix removes exactly those three errors and introduces none.
yield_types.first is ComplexType, nil; calling tuple?/all_params on it
directly failed strong self-typecheck (block.rb:84-85) once merged into
the integration branch, whose narrowing surfaces it. CI on the
integration merge reported exactly these two problems.

SKIP=Solargraph: the Overcommit hook wrapper errors out ('Solargraph
failed to run') in this worktree; the CI 'Solargraph / strong' job is
the authoritative gate.
apiology added a commit that referenced this pull request Aug 17, 2026
Fixes the two strong self-typecheck problems the previous merge
introduced at pin/block.rb:84-85 (yield_types.first is nilable).
add_mlhs_locals recurses on a nested mlhs child (e.g. |((a, b), c)|);
only the single-level group was tested.
Only the all-resolved and none-resolved shapes were tested. When
positions correspond one-to-one but one position's type is an unbound
generic that never binds, typify_parameters must still keep the
sibling position that did resolve, rather than discarding the whole
partial result.

Split from the 2026-08-04 integration branch's bundled undercover
coverage commit 4aab8b2.
lekemula/solargraph-rspec's convention_spec.rb still expects plain
Hash/Array/NilClass, so it fails against this PR's more precise
Hash{Symbol => String}/Array<Integer>/nil literal-inference output.
apiology/solargraph-rspec:fix-hash-literal-type-precision tolerates
both, pending lekemula/solargraph-rspec#36.
apiology added a commit that referenced this pull request Sep 3, 2026
These examples were marked pending on #53 by
#60. The feature that PR was waiting on already
landed via castwide#1231, castwide#1258, and castwide#1312, so the pending wrapper now hides
passing coverage instead of documenting a known gap. RSpec confirmed
each example passes cleanly with the wrapper removed.
apiology added a commit that referenced this pull request Sep 5, 2026
Brings #60 up to its current head. The lib-side work
is already here, so what arrives is
spec/type_checker/levels/destructuring_spec.rb - the coverage for the
two undercover nodes that PR owns, in args_node.rb and pin/block.rb.

One conflict, .github/workflows/plugins.yml, resolved to this branch.
That side removes four continue-on-error suppressions and enables the
bundler cache; the goal here is to surface problems, not to keep the
check green. Both sides clone the same solargraph-rspec branch, so that
part of the conflict was cosmetic.

2186 examples, 0 failures, 45 pending.
Resolves the branch conflict blocking the PR. The only conflict was in
.github/workflows/plugins.yml, where both sides redirect the
run_solargraph_rspec_specs job at apiology/solargraph-rspec branch
fix-hash-literal-type-precision, pending
lekemula/solargraph-rspec#36.

Both spellings clone the same branch of the same fork. Took the base
branch version, a single "git clone --branch", over this branch version,
a "git clone" followed by a separate "git checkout". The resolved file is
byte-identical to the base branch version, so this branch contributes no
plugins.yml change of its own any more.
@apiology
apiology marked this pull request as ready for review September 6, 2026 23:43
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