Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR implements sentinel literal types to support type narrowing with is/is not operations on Final variables, as part of addressing issue #650 for comprehensive type guard support.
Changes:
- Added
LitSentinelvariant to theLitenum for representing sentinel values - Implemented
final_sentinel_literal_for_exprto detect Final sentinel values and convert them to literal types - Integrated sentinel narrowing into
is/is notnarrowing operations - Updated type display and output handling for sentinel literals
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
pyrefly/lib/test/narrow.rs |
Added test cases for Final bool and object sentinel narrowing |
pyrefly/lib/alt/narrow.rs |
Implemented sentinel detection and integrated into narrowing logic |
crates/pyrefly_types/src/literal.rs |
Added LitSentinel struct and related trait implementations |
crates/pyrefly_types/src/display.rs |
Added display formatting for sentinel literals |
crates/pyrefly_types/src/type_output.rs |
Added output handling for sentinel literals |
Cargo.lock |
Automatic checksum update for backtrace dependency |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
crates/pyrefly_types/src/literal.rs
Outdated
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
| #[derive(Visit, VisitMut, TypeEq)] | ||
| pub struct LitSentinel { | ||
| pub module: ModuleName, | ||
| pub name: Name, | ||
| pub range: Option<TextRange>, | ||
| pub class: ClassType, | ||
| } | ||
|
|
||
| impl Ord for LitSentinel { | ||
| fn cmp(&self, other: &Self) -> Ordering { | ||
| (&self.module, &self.name, &self.class).cmp(&(&other.module, &other.name, &other.class)) | ||
| } | ||
| } | ||
|
|
||
| impl PartialOrd for LitSentinel { | ||
| fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||
| Some(self.cmp(other)) | ||
| } | ||
| } |
There was a problem hiding this comment.
The derived PartialEq, Eq, and Hash traits on line 71 compare/hash all fields including range, but the manual Ord and PartialOrd implementations exclude range from comparison. This is inconsistent and violates Rust's requirements:
Ordmust be consistent withPartialEq: if two values are equal bycmp, they must be equal byPartialEqHashmust be consistent withPartialEq: if two values are equal byPartialEq, they must have the same hash
Currently, two LitSentinel values that differ only in their range field would be:
- Unequal by
PartialEq(includesrange) - Equal by
Ord(excludesrange) - Potentially different hashes (includes
range)
This violates constraint 1. The fix is to manually implement PartialEq, Eq, and Hash to exclude range, matching the Ord behavior. See how similar types in the codebase handle this pattern.
yangdanny97
left a comment
There was a problem hiding this comment.
Hmm, I don't like adding a new literal case to represent a potentially non-literal type.
This is the implementation I have in mind:
- at binding phase, if we see a
is <some variable>we can generate a IsSentinel/IsNotSentinel narrow-op that holds 1) the variable binding idx and 2) the annotation idx - when that narrow op is solved it looks up the variable, looks up the annotation, and narrows the subject if the annotation is final. if it isn't final then we do nothing
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Diff from mypy_primer, showing the effect of this PR on open source code: urllib3 (https://github.com/urllib3/urllib3)
- ERROR src/urllib3/util/timeout.py:128:16-79: Returned type `_TYPE_DEFAULT | float | None` is not assignable to declared return type `float | None` [bad-return]
- ERROR src/urllib3/util/timeout.py:150:19-24: Argument `_TYPE_DEFAULT | float` is not assignable to parameter `x` with type `Buffer | SupportsFloat | SupportsIndex | str` in function `float.__new__` [bad-argument-type]
- ERROR src/urllib3/util/timeout.py:158:16-26: `<=` is not supported between `_TYPE_DEFAULT` and `Literal[0]` [unsupported-operation]
- ERROR src/urllib3/util/timeout.py:270:24-34: Returned type `_TYPE_DEFAULT | float` is not assignable to declared return type `float | None` [bad-return]
- ERROR src/urllib3/util/timeout.py:271:30-84: No matching overload found for function `min` called with arguments: (float, _TYPE_DEFAULT | float) [no-matching-overload]
+ ERROR src/urllib3/util/timeout.py:271:23-85: No matching overload found for function `max` called with arguments: (Literal[0], float) [no-matching-overload]
- ERROR src/urllib3/util/timeout.py:271:31-71: `-` is not supported between `_TYPE_DEFAULT` and `float` [unsupported-operation]
- ERROR src/urllib3/util/timeout.py:273:27-67: `-` is not supported between `_TYPE_DEFAULT` and `float` [unsupported-operation]
- ::error file=src/urllib3/util/timeout.py,line=128,col=16,endLine=128,endColumn=79,title=Pyrefly bad-return::Returned type `_TYPE_DEFAULT | float | None` is not assignable to declared return type `float | None`
- ::error file=src/urllib3/util/timeout.py,line=150,col=19,endLine=150,endColumn=24,title=Pyrefly bad-argument-type::Argument `_TYPE_DEFAULT | float` is not assignable to parameter `x` with type `Buffer | SupportsFloat | SupportsIndex | str` in function `float.__new__`
- ::error file=src/urllib3/util/timeout.py,line=158,col=16,endLine=158,endColumn=26,title=Pyrefly unsupported-operation::`<=` is not supported between `_TYPE_DEFAULT` and `Literal[0]`%0A Argument `_TYPE_DEFAULT` is not assignable to parameter `value` with type `int` in function `int.__ge__`
- ::error file=src/urllib3/util/timeout.py,line=270,col=24,endLine=270,endColumn=34,title=Pyrefly bad-return::Returned type `_TYPE_DEFAULT | float` is not assignable to declared return type `float | None`
- ::error file=src/urllib3/util/timeout.py,line=271,col=30,endLine=271,endColumn=84,title=Pyrefly no-matching-overload::No matching overload found for function `min` called with arguments: (float, _TYPE_DEFAULT | float)%0A Possible overloads:%0A (arg1: SupportsRichComparisonT, arg2: SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, *, key: None = None) -> SupportsRichComparisonT [closest match]%0A (arg1: _T, arg2: _T, /, *_args: _T, *, key: (_T) -> SupportsRichComparison) -> _T%0A (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None) -> SupportsRichComparisonT%0A (iterable: Iterable[_T], /, *, key: (_T) -> SupportsRichComparison) -> _T%0A (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T) -> SupportsRichComparisonT | _T%0A (iterable: Iterable[_T1], /, *, key: (_T1) -> SupportsRichComparison, default: _T2) -> _T1 | _T2
+ ::error file=src/urllib3/util/timeout.py,line=271,col=23,endLine=271,endColumn=85,title=Pyrefly no-matching-overload::No matching overload found for function `max` called with arguments: (Literal[0], float)%0A Possible overloads:%0A (arg1: SupportsRichComparisonT, arg2: SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, *, key: None = None) -> SupportsRichComparisonT [closest match]%0A (arg1: _T, arg2: _T, /, *_args: _T, *, key: (_T) -> SupportsRichComparison) -> _T%0A (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None) -> SupportsRichComparisonT%0A (iterable: Iterable[_T], /, *, key: (_T) -> SupportsRichComparison) -> _T%0A (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T) -> SupportsRichComparisonT | _T%0A (iterable: Iterable[_T1], /, *, key: (_T1) -> SupportsRichComparison, default: _T2) -> _T1 | _T2
- ::error file=src/urllib3/util/timeout.py,line=271,col=31,endLine=271,endColumn=71,title=Pyrefly unsupported-operation::`-` is not supported between `_TYPE_DEFAULT` and `float`%0A Argument `_TYPE_DEFAULT` is not assignable to parameter `value` with type `float` in function `float.__rsub__`
- ::error file=src/urllib3/util/timeout.py,line=273,col=27,endLine=273,endColumn=67,title=Pyrefly unsupported-operation::`-` is not supported between `_TYPE_DEFAULT` and `float`%0A Argument `_TYPE_DEFAULT` is not assignable to parameter `value` with type `float` in function `float.__rsub__`
sphinx (https://github.com/sphinx-doc/sphinx)
- ERROR sphinx/theming.py:542:65-81: Object of class `EllipsisType` has no attribute `split` [missing-attribute]
- ERROR sphinx/theming.py:550:62-75: Object of class `EllipsisType` has no attribute `split` [missing-attribute]
+ ERROR sphinx/theming.py:542:53-87: No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[
+ (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString
+ (self: str, chars: str | None = None, /) -> str
+ ], list[str] | Unknown) [no-matching-overload]
+ ERROR sphinx/theming.py:550:50-81: No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[
+ (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString
+ (self: str, chars: str | None = None, /) -> str
+ ], list[str] | Unknown) [no-matching-overload]
- ::error file=sphinx/theming.py,line=542,col=65,endLine=542,endColumn=81,title=Pyrefly missing-attribute::Object of class `EllipsisType` has no attribute `split`
- ::error file=sphinx/theming.py,line=550,col=62,endLine=550,endColumn=75,title=Pyrefly missing-attribute::Object of class `EllipsisType` has no attribute `split`
+ ::error file=sphinx/theming.py,line=542,col=53,endLine=542,endColumn=87,title=Pyrefly no-matching-overload::No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[%0A (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString%0A (self: str, chars: str | None = None, /) -> str%0A], list[str] | Unknown)%0A Possible overloads:%0A (cls: type[map[_S]], func: (_T1) -> _S, iterable: Iterable[_T1], /) -> map[_S] [closest match]%0A (cls: type[map[_S]], func: (_T1, _T2) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], /) -> map[_S]%0A (cls: type[map[_S]], func: (_T1, _T2, _T3) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /) -> map[_S]%0A (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], /) -> map[_S]%0A (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4, _T5) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], iter5: Iterable[_T5], /) -> map[_S]%0A (cls: type[map[_S]], func: (...) -> _S, iterable: Iterable[Any], iter2: Iterable[Any], iter3: Iterable[Any], iter4: Iterable[Any], iter5: Iterable[Any], iter6: Iterable[Any], /, *iterables: Iterable[Any]) -> map[_S]
+ ::error file=sphinx/theming.py,line=550,col=50,endLine=550,endColumn=81,title=Pyrefly no-matching-overload::No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[%0A (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString%0A (self: str, chars: str | None = None, /) -> str%0A], list[str] | Unknown)%0A Possible overloads:%0A (cls: type[map[_S]], func: (_T1) -> _S, iterable: Iterable[_T1], /) -> map[_S] [closest match]%0A (cls: type[map[_S]], func: (_T1, _T2) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], /) -> map[_S]%0A (cls: type[map[_S]], func: (_T1, _T2, _T3) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /) -> map[_S]%0A (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], /) -> map[_S]%0A (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4, _T5) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], iter5: Iterable[_T5], /) -> map[_S]%0A (cls: type[map[_S]], func: (...) -> _S, iterable: Iterable[Any], iter2: Iterable[Any], iter3: Iterable[Any], iter4: Iterable[Any], iter5: Iterable[Any], iter6: Iterable[Any], /, *iterables: Iterable[Any]) -> map[_S]
|
|
@yangdanny97 has imported this pull request. If you are a Meta employee, you can view this in D94090300. |
|
There's a potential performance regression here, I'll investigate when I have time. |
Summary
Fixes part of #650
Added a sentinel literal type and wired is/is not narrowing to use final-name initializers, so Final sentinels now narrow even when the declared type isn’t literal. Updated literal display/output handling.
Test Plan
Added narrowing tests for Final bool and object sentinels.