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

[red knot] Fix narrowing for '… is not …' type guards, add '… is …' type guards #13758

Merged
merged 11 commits into from
Oct 15, 2024

Conversation

sharkdp
Copy link
Contributor

@sharkdp sharkdp commented Oct 15, 2024

Summary

  • Fix a bug with … is not … type guards.

    Previously, in an example like

    x = [1]
    y = [1]
    
    if x is not y:
        reveal_type(x)

    we would infer a type of list[int] & ~list[int] == Never for x inside the conditional (instead of list[int]), since we built a (negative) intersection with the type of the right hand side (y). However, as this example shows, this assumption can only be made for singleton types (types with a single inhabitant) such as None.

  • Add support for … is … type guards.

closes #13715

Test Plan

Moved existing narrow_… tests to Markdown-based tests and added new ones (including a regression test for the bug described above). Note that will create some conflicts with #13719. I tried to establish the correct organizational structure as proposed in this comment

@sharkdp sharkdp added the red-knot Multi-file analysis & type inference label Oct 15, 2024
Copy link
Contributor

github-actions bot commented Oct 15, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

LGTM, but let's wait for at least one review from @AlexWaygood or @carljm to verify that it has the correct semantics

crates/red_knot_python_semantic/src/types.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types.rs Show resolved Hide resolved
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Thanks! This mostly looks good, but there's an important correctness thing we need to fix involving tuples. I also left a couple of minor nitpick comments as well :-)

As well as the language reference, Brett Cannon's blog series unravelling syntactic sugar is also an invaluable resource for understanding what these operators are doing under the hood. His article unravelling is and is not is one of the shorter ones, but it's here: https://snarky.ca/unravelling-is-and-is-not/

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

A couple more nits, but this LGTM now overall. Thanks!

crates/red_knot_python_semantic/src/types.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types.rs Outdated Show resolved Hide resolved
@sharkdp sharkdp merged commit 74bf4b0 into main Oct 15, 2024
20 checks passed
@sharkdp sharkdp deleted the david/type-narrowing-updates branch October 15, 2024 12:49
Comment on lines +489 to +495
// We deliberately deviate from the language specification [1] here and claim
// that the empty tuple type is a singleton type. The reasoning is that `()`
// is often used as a sentinel value in user code. Declaring the empty tuple to
// be of singleton type allows us to narrow types in `is not ()` conditionals.
//
// [1] https://docs.python.org/3/reference/expressions.html#parenthesized-forms
tuple.elements(db).is_empty()
Copy link
Contributor

@carljm carljm Oct 15, 2024

Choose a reason for hiding this comment

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

I'm not sure we'll want to stick with this conclusion, given the response to https://discuss.python.org/t/should-we-specify-in-the-language-reference-that-the-empty-tuple-is-a-singleton/67957/7 -- in particular the fact that is comparisons with tuples are now a syntax warning in recent Python versions, and that there are Python implementations (GraalPy) for which the empty tuple is not a singleton. But we can leave it for now, I don't think it's particularly critical either way.

Copy link
Member

Choose a reason for hiding this comment

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

yeah. Though strictly speaking the narrowing is still safe on all popular Python implementations, and probably always will be. But probably not worth carving out a special case for, given the language appears to be actively discouraging it now.

// all singleton types, but it is not straightforward to compute this. Again,
// we simply return false.
//
// bool & ~Literal[False]`
Copy link
Contributor

Choose a reason for hiding this comment

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

We should (but don't yet) simplify this to just Literal[True] in intersection builder.

// we simply return false.
//
// bool & ~Literal[False]`
// None & (None | int)
Copy link
Contributor

Choose a reason for hiding this comment

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

This type also cannot occur in our representation, because we normalize to disjunctive normal form (union of intersections), so this would become (None & None) | (None & int) and then None | (None & int) and then (this step not yet implemented) None | Never and thus just None.

//
// bool & ~Literal[False]`
// None & (None | int)
// (A | B) & (B | C) with A, B, C disjunct and B a singleton
Copy link
Contributor

@carljm carljm Oct 15, 2024

Choose a reason for hiding this comment

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

We already always normalize this to (A & B) | (A & C) | (B & B) | (B & C) -> (A & B) | (A & C) | B | (B & C), which should (though I'm not sure if it would yet?) become (A & C) | B, which (if we know A and C are disjoint -- this part not yet implemented) should become just B -- so then would return true from this method :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[red-knot] Type narrowing for x is None
4 participants