Skip to content

Fix if-branch pruning for Final bool constants#3571

Open
LeSingh1 wants to merge 1 commit into
facebook:mainfrom
LeSingh1:fix-final-bool-control-flow
Open

Fix if-branch pruning for Final bool constants#3571
LeSingh1 wants to merge 1 commit into
facebook:mainfrom
LeSingh1:fix-final-bool-control-flow

Conversation

@LeSingh1

Copy link
Copy Markdown
Contributor

Summary

Fixes #3537.

When a variable is declared Final and assigned a bool literal (False or True), control-flow tests like if flag: now prune unreachable branches the same way as literal if False: / if True:.

Before this change, both arms of an if/else could contribute to merged types even when the condition was statically known to be always false via Final = False.

Test plan

  • Added test_final_bool_unreachable_if_branchFinal = False → else-only assignment type
  • Added test_final_bool_unreachable_else_branchFinal = True → if-only assignment type
  • cargo test test_final_bool_unreachable

@meta-cla meta-cla Bot added the cla signed label May 24, 2026
@github-actions github-actions Bot added size/m and removed size/m labels May 24, 2026
@Arths17

Arths17 commented May 24, 2026

Copy link
Copy Markdown
Contributor

The idea here makes sense, but the CI failures suggest this change may be leaking beyond Final-declared constants into general boolean control-flow inference.

I’d double-check the branch pruning condition — it should ideally only trigger on Final-backed literals, not inferred constant truthiness.

Might be worth adding a regression test for:

non-Final bool literal assignment
mixed union bool narrowing in if/else joins

to ensure no accidental over-pruning.

@github-actions github-actions Bot added size/m and removed size/m labels May 24, 2026
@LeSingh1
LeSingh1 force-pushed the fix-final-bool-control-flow branch from d6dcf90 to c902270 Compare July 10, 2026 15:04
@github-actions github-actions Bot added size/l and removed size/m labels Jul 10, 2026
@github-actions

This comment has been minimized.

Prune the correct if-branch when the condition is a `Final` bool constant
by narrowing control flow based on the constant's value. Track `Final` bool
literal values (`FLAG: Final = False`) alongside the existing `Final` string
values, and consult them in the branch-pruning path so `if FLAG:` / `else`
branches are statically resolved.

Also make the implicit-return scan consistent with this pruning. The
function-body scan that finds the trailing statement(s) for implicit-return
analysis (`function_last_expressions`) previously pruned only `sys_info`
branches. Once the binding traversal also prunes `Final` bool branches, the
scan could record a trailing statement inside a branch that never gets bound,
producing a dangling `Idx<Key>` and a "key lacking binding" panic at solve
time (observed via mypy_primer on `mypy`'s `mypyc/codegen/emit.py`). The scan
now prunes `Final` bool branches the same way, keeping it in lockstep with the
traversal.
@LeSingh1
LeSingh1 force-pushed the fix-final-bool-control-flow branch from c902270 to 9efd9c0 Compare July 13, 2026 19:53
@github-actions github-actions Bot added size/l and removed size/l labels Jul 13, 2026
@github-actions

Copy link
Copy Markdown

Diff from mypy_primer, showing the effect of this PR on open source code:

apprise (https://github.com/caronc/apprise)
+ ERROR apprise/plugins/vapid/__init__.py:356:20-43: Object of class `dict` has no attribute `load` [missing-attribute]
+ ERROR apprise/plugins/vapid/__init__.py:391:16-22: `not in` is not supported between `object` and `WebPushSubscriptionManager` [unsupported-operation]
+ ERROR apprise/plugins/vapid/__init__.py:405:28-65: Argument `EllipticCurvePublicKey | Unknown | None` is not assignable to parameter `public_key` with type `EllipticCurvePublicKey` in function `apprise.utils.pem.ApprisePEMController.encrypt_webpush` [bad-argument-type]
+ ERROR apprise/plugins/vapid/__init__.py:405:47-53: Cannot index into `WebPushSubscriptionManager` [bad-index]
+ ERROR apprise/plugins/vapid/__init__.py:406:29-67: Argument `bytes | Unknown | None` is not assignable to parameter `auth_secret` with type `bytes` in function `apprise.utils.pem.ApprisePEMController.encrypt_webpush` [bad-argument-type]
+ ERROR apprise/plugins/vapid/__init__.py:406:48-54: Cannot index into `WebPushSubscriptionManager` [bad-index]
+ ERROR apprise/plugins/vapid/__init__.py:609:42-70: Argument `bytes | None` is not assignable to parameter `data` with type `bytes` in function `apprise.utils.base64.base64_urlencode` [bad-argument-type]

@meta-codesync

meta-codesync Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@connernilsen has imported this pull request. If you are a Meta employee, you can view this in D113574404.

@connernilsen connernilsen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @LeSingh1, thanks for working on this! Here are a few questions/recommendations real quick

print("pruned and last")
"#,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test to validate that a closer, non-final bool definition doesn't prune branches too early?

Probably something like this:

from typing import Final

flag: Final = True
# this should hit that early return case where a local scope might override a more global scope
def foo(flag: bool):
  if flag:
      bar = 1
  else:
      bar = 2
  reveal_type(bar)  # E: revealed type: Literal[1] | Literal[2] (I'm not sure if this would be literal or collapse to just `int`)

Comment on lines +141 to +161
testcase!(
test_final_bool_if_branch_implicit_return_no_panic,
r#"
from typing import Final
FLAG_FALSE: Final = False
FLAG_TRUE: Final = True
def prune_if() -> None:
if FLAG_FALSE:
print("pruned")
else:
print("kept")
def prune_else() -> None:
if FLAG_TRUE:
print("kept")
else:
print("pruned")
def prune_if_no_else() -> None:
if FLAG_FALSE:
print("pruned and last")
"#,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add one more check in this test for function-level finals, since those might crash at runtime due to the branch being dropped

def prune_if_no_else_local() -> None:
  FLAG_FALSE: Final = False
  if FLAG_FALSE
    print("pruned and last")
def prune_else_local() -> None:
  FLAG_FALSE: Final = False
  if FLAG_FALSE
    print("pruned")
  else:
    print("kept")
def prune_if_local() -> None:
  FLAG_TRUE: Final = False
  if FLAG_TRUE
    print("kept")
  else:
    print("pruned")

You may need to fix this by computing implicit_return after the function scope's static state is initialized (inside or after init_static_scope) so both function_last_expressions -> evaluate_bool_for_control_flow and stmt.rs:1265 see the same final_bool_values.

/// used to resolve Final variable references in synthesized class field names.
pub final_names: SmallMap<Name, Option<String>>,
/// Names marked `Final` with bool literal values, e.g. `FLAG: Final = False`.
/// Used to statically evaluate `if FLAG:` / `if not FLAG:` control flow.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might need to fix the evaluator here (or add a test validating that it does this). I don't think the current logic can handle not FLAG, since we only special case a bare Expr::Name below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

always True & always False variables are incorrectly treated as if they could be either

4 participants