Skip to content

impl Sentinel, part of #650#2252

Closed
asukaminato0721 wants to merge 3 commits intofacebook:mainfrom
asukaminato0721:650
Closed

impl Sentinel, part of #650#2252
asukaminato0721 wants to merge 3 commits intofacebook:mainfrom
asukaminato0721:650

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

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.

@meta-cla meta-cla bot added the cla signed label Jan 29, 2026
@github-actions

This comment has been minimized.

@asukaminato0721 asukaminato0721 marked this pull request as ready for review January 29, 2026 04:48
Copilot AI review requested due to automatic review settings January 29, 2026 04:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 LitSentinel variant to the Lit enum for representing sentinel values
  • Implemented final_sentinel_literal_for_expr to detect Final sentinel values and convert them to literal types
  • Integrated sentinel narrowing into is/is not narrowing 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.

Comment on lines +71 to +90
#[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))
}
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

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:

  1. Ord must be consistent with PartialEq: if two values are equal by cmp, they must be equal by PartialEq
  2. Hash must be consistent with PartialEq: if two values are equal by PartialEq, they must have the same hash

Currently, two LitSentinel values that differ only in their range field would be:

  • Unequal by PartialEq (includes range)
  • Equal by Ord (excludes range)
  • 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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@yangdanny97 yangdanny97 left a comment

Choose a reason for hiding this comment

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

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

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync bot commented Feb 23, 2026

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

@yangdanny97
Copy link
Copy Markdown
Contributor

There's a potential performance regression here, I'll investigate when I have time.

@yangdanny97 yangdanny97 reopened this Mar 11, 2026
@github-actions
Copy link
Copy Markdown

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]

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]

@github-actions
Copy link
Copy Markdown

Primer Diff Classification

✅ 2 improvement(s) | 2 project(s) total | +1, -9 errors

2 improvement(s) across urllib3, sphinx.

Project Verdict Changes Error Kinds Root Cause
urllib3 ✅ Improvement +1, -7 bad-argument-type, bad-return final_sentinel_type_for_binding()
sphinx ✅ Improvement -2 missing-attribute final_sentinel_type_for_binding()
Detailed analysis

✅ Improvement (2)

urllib3 (+1, -7)

This is an improvement. The PR implemented better sentinel value handling for Final variables. Looking at the code, _DEFAULT_TIMEOUT is a Final sentinel of type _TYPE_DEFAULT (an enum). The removed errors were false positives:

  1. unsupported-operation on _DEFAULT_TIMEOUT <= 0 (line 158) - this comparison is intentionally handled in a try/except block and is valid sentinel usage
  2. bad-return from resolve_default_timeout() (line 128) - the function correctly returns either getdefaulttimeout() or the timeout parameter, both valid for the float | None return type
  3. bad-argument-type for float(value) (line 150) - this is in a try/except block specifically to validate that non-sentinel values can be converted to float
  4. no-matching-overload for min() with sentinel - the sentinel is filtered out by the conditional checks before min() is called

The new error on line 271 max(0, min(self.total - self.get_connect_duration(), self._read)) appears to be a legitimate issue where the second argument to max() is float (from min() result) but max(Literal[0], float) doesn't match the expected overloads.

The PR's sentinel narrowing improvements correctly removed false positives while potentially exposing a real type issue.

Attribution: The changes to final_sentinel_type_for_binding() and IsSentinel/IsNotSentinel narrowing operations in pyrefly/lib/alt/narrow.rs and pyrefly/lib/binding/narrow.rs improved sentinel value handling. The new sentinel_narrow_op_for_expr() function now recognizes Final sentinel patterns and narrows types appropriately, which removed the false positive errors about _DEFAULT_TIMEOUT operations.

sphinx (-2)

Looking at the source code, lines 537 and 545 use fallback=... (ellipsis) in configparser.get() calls. The variables stylesheet and sidebar can be either strings or ellipsis. Lines 542 and 550 then call .split(',') on these variables without checking if they are ellipsis first. Since ellipsis has no split method, this would cause AttributeError at runtime. The PR improved Final sentinel narrowing, allowing pyrefly to correctly infer that these variables could be EllipsisType, thus catching the real bug where .split() is called on ellipsis. This is a genuine type error that would cause runtime crashes.
Attribution: The changes to final_sentinel_type_for_binding() and sentinel narrowing logic in pyrefly/lib/alt/narrow.rs improved type inference for Final sentinels. This better narrowing caused pyrefly to infer that the variables stylesheet and sidebar could be ellipsis (...) instead of just strings, which then correctly flagged the invalid .split() calls.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (2 LLM)

@yangdanny97
Copy link
Copy Markdown
Contributor

yangdanny97 commented Mar 20, 2026

I'm working on merging this one, needs a bit of splitting up to make it reviewable internally. No action needed from you ATM, thanks

@yangdanny97
Copy link
Copy Markdown
Contributor

I was looking at the code here, and I think we may already handle is/is not narrowing correctly for Final variables, as long as it's not explicitly annotated with a wider type. (and if it is, we should respect that wider type).

I think my guidance on #650 was actually wrong re: this being about final variables, looking at the history of that feature in Pyright this seems to actually be for PEP 661, which is not part of the typing spec (yet). The Sentinel type was added to typing_extensions for implementation, but the PEP is not accepted.

microsoft/pyright#10565

In that case, I'm sorry I sent you down the wrong path here, I really appreciate the work here still

@asukaminato0721 asukaminato0721 deleted the 650 branch March 21, 2026 05:24
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.

3 participants