Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

check_boolean_op: clean up semanal/type-based reachability #10566

Merged
merged 4 commits into from
Jun 16, 2021

Conversation

ikonst
Copy link
Contributor

@ikonst ikonst commented Jun 1, 2021

ExpressionChecker.check_boolean_op supports two potentially conflicting ways of determining whether an expression is always true/false: name-based checks from semantic analysis (e.g. name in options.always_true) and type narrowing in TypeChecker.find_isinstance_check (*). The two ways can be set to conflict, e.g.

# flags: --always-true COMPILE_TIME_TRUE
COMPILE_TIME_TRUE: None

In the above case, COMPILE_TIME_TRUE would be always true per semanal and always false per find_isinstance_check, which results in an assertion failure here:

assert left_map is not None # find_isinstance_check guarantees this

and here:
assert right_map is not None # find_isinstance_check guarantees this

This change reconciles the two, by prioritizing the semanal flags (e.right_always, e.right_unreachable) over type-narrowing (if_map, else_map). Then we explicitly avoid flagging reachability errors when the semanal flags are set, since they're obviously used intentionally.

(*) which seems to do slightly more than isinstance checks

@ikonst ikonst changed the title Add regression tests Streamline reachability and check_boolean_op Jun 1, 2021
@ikonst ikonst changed the title Streamline reachability and check_boolean_op check_boolean_op: clean up semanal/type-based reachability Jun 1, 2021
if e.op == 'and':
left_map: mypy.checker.TypeMap
right_map: mypy.checker.TypeMap
if e.right_always:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For always-true/false cases, find_isinstance_check returns a {}, None or a None, {} pair. We do the same here, but without even calling find_isinstance_check.

@@ -2822,6 +2818,13 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
assert right_map is not None # find_isinstance_check guarantees this
return right_type

if e.op == 'and':
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can safely move it down here since nothing uses restricted_left_type and result_is_left until this point.

@ikonst
Copy link
Contributor Author

ikonst commented Jun 2, 2021

@hauntsaninja @JukkaL want to take a look?

@ikonst
Copy link
Contributor Author

ikonst commented Jun 15, 2021

@hauntsaninja @JukkaL gentle ping

Copy link
Collaborator

@hauntsaninja hauntsaninja left a comment

Choose a reason for hiding this comment

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

Sorry for the delay and thanks for this well explained PR that fixes a crash!
However, the reveal types in your test cases aren't quite what I'd expect. E.g., in your first test case I'd expect reveal_type(b) to be bool (Literal[True] and None shouldn't really be appearing anywhere, since mypy should always think the COMPILE_TIME_* are True or False).
I think we should maybe set left_type and right_type if e.right_always or e.right_unreachable? Also happy to merge as is!

@ikonst
Copy link
Contributor Author

ikonst commented Jun 16, 2021

However, the reveal types in your test cases aren't quite what I'd expect. E.g., in your first test case I'd expect reveal_type(b) to be bool (Literal[True] and None shouldn't really be appearing anywhere, since mypy should always think the COMPILE_TIME_* are True or False).

You're saying you'd expect:

 indeterminate: bool
 COMPILE_TIME_FALSE: Literal[True]
 b = indeterminate or COMPILE_TIME_FALSE
-reveal_type(b)  # N: Revealed type is "Union[builtins.bool, Literal[True]]"
+reveal_type(b)  # N: Revealed type is "builtins.bool"

but we don't know if indeterminate is truthy; if it is, the expression's type is LHS type, else, it's the RHS type (never mind if RHS is truthy or not).

Or in this case you'd expect:

 indeterminate: bool
 COMPILE_TIME_TRUE: None
 b = indeterminate or COMPILE_TIME_TRUE
-reveal_type(b)  # N: Revealed type is "Union[builtins.bool, None]"
+reveal_type(b)  # N: Revealed type is "builtins.bool"

Same logic. COMPILE_TIME_TRUE is typed None, never mind if it's truthy or not. The expression should have either LHS or RHS type -- since LHS is indeterminate, a union of both.

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Jun 16, 2021

mypy --help says:

  --always-true NAME        Additional variable to be considered True (may be repeated)
  --always-false NAME       Additional variable to be considered False (may be repeated)

which I interpreted as the variable should be interpreted as that literal boolean value, not just something that's truthy or falsey (TYPE_CHECKING and MYPY are often used to make bold lies to the type checker, so just wiping out the type seemed like a valid interpretation).
But https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-always-true reads much more ambiguously, it seems you didn't read it that way, and changing it would require more work for something that's kind of niche. So like I said, happy to merge, and thank you again!

@hauntsaninja hauntsaninja merged commit 790ab35 into python:master Jun 16, 2021
@ikonst
Copy link
Contributor Author

ikonst commented Jun 16, 2021

the variable should be interpreted as that literal boolean value, not just something that's truthy or falsey

That's what I'd assume too, and maybe that'd be a better way to implement it. As for now, those options manifest in the OpExpr. right_always and OpExpr.right_unreachable (a semanal artifact) which are booleans used purely for this feature. I've added comments to document that:

-    # Is the right side going to be evaluated every time?
+    # Per static analysis only: Is the right side going to be evaluated every time?
     right_always = False
-    # Is the right side unreachable?
+    # Per static analysis only: Is the right side unreachable?
     right_unreachable = False

@ikonst ikonst deleted the fix-always-false-flag branch June 16, 2021 14:02
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.

2 participants