Don't treat a breakless default case as a switch terminator - #187
Open
junoatwork wants to merge 1 commit into
Open
Don't treat a breakless default case as a switch terminator#187junoatwork wants to merge 1 commit into
junoatwork wants to merge 1 commit into
Conversation
A `default` case that falls off its end without break/return leaves the
switch normally, exactly like an explicit break. control_analyzer was
missing this: such a default yielded case_actions = {None}, which skipped
the break/continue/leave-switch check and unconditionally set
has_default_terminator = true, so the switch's control actions were
reported as always-returning.
Downstream, ifelse_analyzer treats a guard block whose statements never
fall through as always-exiting and applies the else-branch (negated) type
narrowing to the continuation. So a guard like
if (!$x is Foo) {
switch (...) { case A: return ...; default: /* no break */ }
/* no return */
}
// $x used here
wrongly narrowed $x to Foo after the block, even though control can reach
that use with $x still the original union.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Problem
hakana over-narrows a variable on the path after a guard block when that block ends in a
switchwhosedefaultcase falls off the end withoutbreak/return. It concludes the guard always exits, so it applies the guard's negated type narrowing to the continuation — even though control actually reaches the continuation with the variable still its original (wider) type.hh_clienttypes the variable as the union here; hakana should too.Solution
In
control_analyzer'sswitchhandling, treat adefaultcase whose control actions containNone(i.e. it falls off its end) the same as an explicitbreak/continue/LeaveSwitch: the switch is not a control-flow terminator, so the enclosing block does not always exit. Thedefaultcase is always last, so falling off its end leaves the switch normally.A reviewer only needs the one added condition in
get_control_actions; the chain from there to the observed over-narrowing is in the fold.Regression test
SwitchType/defaultFallthroughDoesNotNarrowGuardreproduces the exact shape (guard-if ending in a breakless-defaultswitch, no trailing return) and asserts the guarded variable stays a union afterward.Verification
NonExistentMethodthat proves the variable is still a union is not emitted) and passes with it.tests/inference(incl. all 40SwitchTypecases),tests/unused,tests/nopanic, andtests/securitysuites green. The one pre-existingtests/diff/preserveOverlappingDuplicateDefinitionfailure reproduces identically onmainwithout this change (unrelated to switch control flow).Mechanism, and how the fix is contained
Why the over-narrowing happened. For a breakless
defaultthat falls off the end,get_control_actionscomputescase_actions = {None}. The pre-fix code:break/continue/LeaveSwitchcheck at the top of the default-case block does not match{None}, so it doesn'tcontinue 'outer.case_does_endisfalse(Noneis neitherEndnorReturn), andhas_endedstaysfalse.has_default_terminator = trueis then set unconditionally, and the switch returns its control actions as{Return}— droppingNone.So the switch is reported as always returning. Upstream,
ifelse_analyzer.rs(~line 263) treats a then/guard block whose statements never yieldNoneas always-exiting and applies the else-branch (negated) narrowing to the continuation — narrowing$xtoSubafter the guard, even though control reaches the continuation with$xstill the original union.The fix adds
|| case_actions.contains(&ControlAction::None)to the existing early-continue 'outercondition, so a fall-off-end default is handled exactly like abreak: the switch falls through,Noneis inserted for the block, and the block is no longer seen as always-exiting.Containment. The change only affects the default-case branch, and only when that case falls off its end (
{None}). Defaults thatbreak/continue/return/exitalready hit the existing conditions and are unchanged; this only adds the previously-missing fall-off-end case to the same early-exit path. Verified by the fullSwitchTypesuite (40 tests) plus the inference/unused/nopanic/security suites staying green.