fix(types): use structural termination analysis for post-guard narrowing fallthrough#599
Merged
nahime0 merged 5 commits intoJul 25, 2026
Conversation
…ing fallthrough `Checker::body_cannot_fall_through()` only inspected `body.last()` against a small set of direct statement kinds, so a post-`if`-guard narrowing was dropped whenever the guard body could not fall through only through structure the last-statement check missed: a terminal statement before unreachable trailing code, or a nested `if`/`switch`/`try` whose branches all terminate. Delegate the fallthrough decision to the structural control-flow analysis in `src/termination.rs` (`block_terminal_effect`), which already models return/throw/break/continue and nested terminal forms by effect kind. This uses the "cannot reach the following statement" notion — `break`/`continue` are valid non-fallthrough effects — rather than "guarantees function exit". The narrowing-specific divergence the structural model does not track (`exit()`, `die()`, and user functions declared `never`, which needs the checker's function table) is preserved as an overlay that now also fires when the diverging call precedes unreachable code. Fixes illegalstudio#590. Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
Member
|
Updated this PR in place and merged the current Follow-up after the structural termination review:
The semantic distinction remains intentional: Focused validation on the published head:
Published head: GitHub currently reports the PR as mergeable and aligned with |
…uctural-termination
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.
Summary
Fixes #590.
Checker::body_cannot_fall_through()— the helper that decides whether anif-guard body prevents fall-through, so the complement narrowing can be kept for the statements after the guard — only inspectedbody.last()against a small set of direct statement kinds. It therefore dropped a valid post-guard narrowing whenever the guard body could not fall through only through structure that check missed:return; echo "x";);if/switch/trywhose branches all terminate.As a result, a guarded
?array(or any?T) value was not narrowed to its non-null complement after the guard, so e.g. list-unpacking it failed to type-check.Root cause
body_cannot_fall_through()kept its own last-statement-only model instead of using the compiler's structural control-flow analysis insrc/termination.rs.block_terminal_effectalready scans a block for the first non-fallthrough statement and recurses into nestedif/switch/tryby effect kind, so it naturally handles terminal-before-unreachable and all-branches-terminate shapes that the last-statement matcher could not.Fix
Delegate the fallthrough decision to
block_terminal_effect(body) != TerminalEffect::FallsThrough:break/continuecount as non-fallthrough (they prevent reaching the statement after the guard even though they do not exit the function) — distinct from "guarantees function exit". Post-guard narrowing needs exactly this distinction, andsrc/termination.rsalready models the effect kinds (Return/Throw/Continue/Break(n>1)→ExitsCurrentBlock,Break(1)→Breaks).exit(),die(), or a user function declarednever(the last needs the checker's function table). That narrowing-specific divergence is preserved as an overlay (stmt_is_diverging_call), which now also fires when the diverging call precedes unreachable trailing code, not only as the block's last statement.Checker-side only;
src/termination.rsis consumed, not modified.Test plan
Red→green verified against the issue's shapes. New tests:
tests/codegen/types/narrowing.rs(runtime, narrowing kept):test_narrow_after_terminal_return_before_unreachable_code(shape 1)test_narrow_after_nested_if_all_branches_terminate(shape 2)test_narrow_after_nested_switch_all_paths_terminatetest_narrow_after_nested_try_all_paths_terminatetest_narrow_after_continue_before_unreachable_codetest_narrow_after_exit_before_unreachable_codetest_narrow_after_never_call_before_unreachable_codetests/error_tests/narrowing.rs(narrowing correctly NOT kept):test_no_narrow_when_nested_branch_falls_throughtest_no_narrow_when_switch_has_no_defaultFocused runs (all green):
cargo test --test error_tests -- narrowing list_unpack never warningsandcargo test --test codegen_tests -- narrow list_unpack arrays::list_unpack. Existing return/throw/exit/die/declared-nevernarrowing tests and #522's list-unpack tests remain green.Related
May also resolve #508 (nullable narrowing after an
=== nullearly-exit guard): its minimalexit/returnrepro already type-checks on currentmain(resolved by #522), and the nested=== nullearly-exit shapes it describes now narrow correctly with this change.https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L