Skip to content

Add a CompoundStatement parent chain and use it for reassignment override eligibility - #57

Merged
apiology merged 2 commits into
fix-1250-parameter-reassignment-typingfrom
pr1-compound-statement-chain
Aug 14, 2026
Merged

Add a CompoundStatement parent chain and use it for reassignment override eligibility#57
apiology merged 2 commits into
fix-1250-parameter-reassignment-typingfrom
pr1-compound-statement-chain

Conversation

@apiology

@apiology apiology commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Follow-up to castwide#1282, stacked on its head branch. Design and rationale discussed at castwide#1282 (comment) and follow-up conversation.

Summary

PR 1282 fixed a bug where a type-changing reassignment inside a conditional branch didn't override the earlier assignment's type even at a use site inside the same branch, right after the reassignment. That fix worked but duplicated structure the codebase already partially has: Pin::CompoundStatement ("a series of statements where if a later one executes, all earlier ones did too") and Pin::Closure < CompoundStatement ("a CompoundStatement that is also a scope"). This PR removes that duplication.

  • Region#compound_statement: the nearest enclosing CompoundStatement pin, threaded through Region#update the same way closure already is - every construct that creates a CompoundStatement-family pin (or previously had no corresponding pin at all - and/or/orasgn/rescue bodies) now sets this pointer.
  • Pin::Base#closure becomes @closure || <derived from the compound_statement chain>, strictly as a fallback behind the stored value - no behavior change, since every pin built through Region-threaded node processors still passes closure: explicitly.
  • BaseVariable#definite_reaches? no longer compares a query Location against a separately-computed conditional_override_boundary Range. It now checks containment directly against the compound_statement pin's own location.range - the same range, read once instead of computed twice. Region#conditional_boundary, BaseVariable#conditional_override_boundary, and the Range.from_node(...) call in every conditional node processor are gone.
  • Region#conditional (a plain boolean) replaces the old conditional_boundary.nil? check for computing definite - kept separate from the compound_statement chain because a block body pin is a Closure (for scoping) but still runs zero-or-many times, which conditional/compound_statement.is_a?(Closure) would conflate if collapsed into one signal.
  • New specs: an agreement check that the derived closure matches the stored one across nested if/while/block structures (caught a real threading gap before it shipped), a loop-ordering regression (a while-body reassignment must not affect a reference textually before it), and combine_with coverage for Pin::CompoundStatement.

Test plan

  • bundle exec rspec spec/ - 1638 examples, 0 failures
  • bundle exec solargraph typecheck --level strong - diffed against the pre-1282-follow-up baseline: 587 problems vs. 591 baseline (net fewer - deleting the old Range computation also removed several instances of the pre-existing nilable-AST-child pattern already tolerated throughout these files)
  • bundle exec rubocop - clean on touched files
  • Original bug repro from PR 1282's review comment re-verified via the strong_spec.rb regression test added there

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

apiology and others added 2 commits August 14, 2026 13:37
Region now tracks compound_statement (the nearest enclosing
CompoundStatement pin - an if/when/while/until/rescue/&&/||/||=
body, a method/block body, or a namespace body), threaded through
Region#update the same way closure already is. Every construct that
creates a CompoundStatement-family pin, or previously only threaded
conditional_boundary with no corresponding pin, now sets this
pointer, giving every CompoundStatement pin a real link to its
immediate parent instead of only the coarser closure chain (which
already skips non-scope-forming branches like if-bodies).

Pin::Base#closure becomes @closure || <derived by walking the
compound_statement chain to the nearest ancestor that is_a?(Closure)>,
kept strictly as a fallback behind the stored value - hand-built pins
that pass closure: directly and have no derivable chain (send_node.rb's
synthetic attr_reader/attr_writer pins, args_node.rb, etc.) are
untouched. Every pin built through Region-threaded node processors
still passes closure: explicitly today, so this is a no-behavior-change
infra addition, verified by a new spec asserting the derived value
agrees with the stored one across nested if/while/block structures.

Pin::CompoundStatement also gains its own combine_with/
combine_compound_statement for incremental-reparse merging, mirroring
BaseVariable#combine_closure's location-based tiebreak rather than
reusing choose_pin_attr_with_same_name (unsuitable since bare
CompoundStatement pins all share name == '').

BaseVariable also gains a compound_statement reader, threaded from
lvasgn_node.rb, unused by any override logic yet - preparation for a
follow-up that rewrites override_assignments?/definite_reaches? to
walk this chain instead of comparing conditional_override_boundary
Ranges, removing that duplicate bookkeeping. See the discussion on
castwide#1282 for the fix this
builds on and the design rationale for this follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbhZvdCv7xdziXyKJiPuGk
BaseVariable#definite_reaches? no longer compares a query Location
against a separately-stored conditional_override_boundary Range.
Instead it checks whether the location falls within this pin's own
compound_statement's location range - the CompoundStatement pin
already carries that range, and since a nested CompoundStatement's
location is always a subrange of its parent's, this single
containment check already accounts for arbitrarily nested branches
without needing to walk the chain further.

This removes the duplicate bookkeeping the original PR 1282 fix
introduced: Region#conditional_boundary (a Range) and
BaseVariable#conditional_override_boundary are gone, along with the
Range.from_node(...) computation every conditional-construct node
processor performed to populate them - that range is now read
directly off the compound_statement pin instead of being computed a
second time.

lvasgn_node.rb's `definite` computation goes back to a plain
Region#conditional boolean rather than `conditional_boundary.nil?`
(and was briefly, incorrectly, tried as `compound_statement.is_a?
(Closure)` during this rewrite - reverted because a block's body
pin IS a Closure, for variable-scoping purposes, despite running
zero or many times, which is exactly the case
`conditional_boundary`/`conditional` exists to distinguish). Every
closure-creating node processor (def_node.rb, defs_node.rb,
namespace_node.rb) now explicitly resets `conditional: false` for
its body, since entering a fresh method/namespace scope always runs
its body top-to-bottom regardless of how the closure itself was
reached, unlike a block.

Added:
- A loop-ordering regression test confirming a reassignment inside a
  while body doesn't affect a reference textually before it.
- combine_with specs for Pin::CompoundStatement covering the
  location-based tiebreak and the nil-vs-non-nil case.

Verified: full suite (1638 examples, 0 failures), typecheck self-check
diffed against the pre-fix baseline (587 problems vs. 591 baseline -
net fewer, since deleting the Range.from_node calls also removed
several instances of the pre-existing nilable-AST-child pattern
already tolerated throughout these files).

Combines what were originally staged as two follow-up PRs into one -
see castwide#1282 for the base fix
and design discussion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbhZvdCv7xdziXyKJiPuGk
@apiology apiology changed the title PR1: Add a CompoundStatement parent chain; derive closure as a fallback Add a CompoundStatement parent chain and use it for reassignment override eligibility Aug 14, 2026
@apiology
apiology marked this pull request as ready for review August 14, 2026 18:36
@apiology
apiology merged commit 02708aa into fix-1250-parameter-reassignment-typing Aug 14, 2026
27 of 28 checks passed
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