From 20f00706d7fcbf810af99b56b67285af2cfa68e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Mon, 18 May 2026 22:15:31 +0000 Subject: [PATCH 01/11] Add Sentinel support (PEP-661) --- crates/pyrefly_config/src/error_kind.rs | 2 + crates/pyrefly_types/src/display.rs | 1 + crates/pyrefly_types/src/equality.rs | 14 +++ crates/pyrefly_types/src/heap.rs | 6 ++ crates/pyrefly_types/src/lib.rs | 1 + crates/pyrefly_types/src/sentinel.rs | 72 ++++++++++++++ crates/pyrefly_types/src/stdlib.rs | 7 ++ crates/pyrefly_types/src/types.rs | 8 ++ pyrefly/lib/alt/attr.rs | 3 + pyrefly/lib/alt/expr.rs | 92 ++++++++++++++++++ pyrefly/lib/alt/narrow.rs | 18 +++- pyrefly/lib/alt/solve.rs | 23 +++++ pyrefly/lib/binding/binding.rs | 6 ++ pyrefly/lib/binding/stmt.rs | 28 ++++++ pyrefly/lib/export/special.rs | 3 + pyrefly/lib/query.rs | 4 + pyrefly/lib/report/cinderx/convert.rs | 1 + pyrefly/lib/test/mod.rs | 1 + pyrefly/lib/test/narrow.rs | 87 +++++++++++++++++ pyrefly/lib/test/sentinel.rs | 122 ++++++++++++++++++++++++ pyrefly/lib/tsp/type_conversion.rs | 3 + website/docs/error-kinds.mdx | 14 +++ 22 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 crates/pyrefly_types/src/sentinel.rs create mode 100644 pyrefly/lib/test/sentinel.rs diff --git a/crates/pyrefly_config/src/error_kind.rs b/crates/pyrefly_config/src/error_kind.rs index 77ba3fc0df..52c864a3a7 100644 --- a/crates/pyrefly_config/src/error_kind.rs +++ b/crates/pyrefly_config/src/error_kind.rs @@ -225,6 +225,8 @@ pub enum ErrorKind { /// A use of `typing.Self` in a context where Pyrefly does not recognize it as /// mapping to a valid class type. InvalidSelfType, + /// An error caused by incorrect usage or definition of a Sentinel. + InvalidSentinel, /// Attempting to call `super()` in a way that is not allowed. /// e.g. calling `super(Y, x)` on an object `x` that does not match the class `Y`. InvalidSuperCall, diff --git a/crates/pyrefly_types/src/display.rs b/crates/pyrefly_types/src/display.rs index 508e5de926..ce239fe960 100644 --- a/crates/pyrefly_types/src/display.rs +++ b/crates/pyrefly_types/src/display.rs @@ -585,6 +585,7 @@ impl<'a> TypeDisplayContext<'a> { output.write_qname(t.qname())?; output.write_str("]") } + Type::Sentinel(t) => output.write_qname(t.qname()), Type::TypeVarTuple(t) => { let type_var_tuple_qname = self.stdlib.map(|s| s.type_var_tuple().qname()); output.write_builtin("TypeVarTuple", type_var_tuple_qname)?; diff --git a/crates/pyrefly_types/src/equality.rs b/crates/pyrefly_types/src/equality.rs index c4030cf1ad..52a71413c5 100644 --- a/crates/pyrefly_types/src/equality.rs +++ b/crates/pyrefly_types/src/equality.rs @@ -28,6 +28,7 @@ use vec1::Vec1; use crate::param_spec::ParamSpec; use crate::quantified::QuantifiedIdentity; +use crate::sentinel::Sentinel; use crate::type_var::TypeVar; use crate::type_var_tuple::TypeVarTuple; @@ -47,6 +48,7 @@ pub struct TypeEqCtx { param_spec: SmallMap, type_var: SmallMap, type_var_tuple: SmallMap, + sentinel: SmallMap, /// Alpha-equivalence mapping for Quantified binders: LHS identity → RHS identity. /// First pairing wins; subsequent occurrences must match. quantified_identity: SmallMap, @@ -144,6 +146,18 @@ impl TypeEq for TypeVarTuple { } } +impl TypeEq for Sentinel { + fn type_eq(&self, other: &Self, ctx: &mut TypeEqCtx) -> bool { + type_eq_identity( + self, + other, + ctx, + |ctx| &mut ctx.sentinel, + |ctx| self.type_eq_inner(other, ctx), + ) + } +} + pub trait TypeEq: Eq { fn type_eq(&self, other: &Self, ctx: &mut TypeEqCtx) -> bool { let _ = ctx; diff --git a/crates/pyrefly_types/src/heap.rs b/crates/pyrefly_types/src/heap.rs index 135075c582..71c3ed3ba9 100644 --- a/crates/pyrefly_types/src/heap.rs +++ b/crates/pyrefly_types/src/heap.rs @@ -37,6 +37,7 @@ use crate::literal::Literal; use crate::module::ModuleType; use crate::param_spec::ParamSpec; use crate::quantified::Quantified; +use crate::sentinel::Sentinel; use crate::shaped_array::ShapedArrayType; use crate::special_form::SpecialForm; use crate::tuple::Tuple; @@ -137,6 +138,11 @@ impl TypeHeap { Type::None } + /// Create a `Type::Sentinel`. + pub fn mk_sentinel(&self, sentinel: Sentinel) -> Type { + Type::Sentinel(sentinel) + } + /// Create a `Type::Union` from members. pub fn mk_union(&self, members: Vec) -> Type { Type::Union(Box::new(Union { diff --git a/crates/pyrefly_types/src/lib.rs b/crates/pyrefly_types/src/lib.rs index 030c829edc..56955e243a 100644 --- a/crates/pyrefly_types/src/lib.rs +++ b/crates/pyrefly_types/src/lib.rs @@ -38,6 +38,7 @@ pub mod module; pub mod param_spec; pub mod quantified; pub mod read_only; +pub mod sentinel; pub mod shaped_array; pub mod simplify; pub mod special_form; diff --git a/crates/pyrefly_types/src/sentinel.rs b/crates/pyrefly_types/src/sentinel.rs new file mode 100644 index 0000000000..4f11410b79 --- /dev/null +++ b/crates/pyrefly_types/src/sentinel.rs @@ -0,0 +1,72 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use std::fmt; +use std::fmt::Display; +use std::hash::Hash; + +use dupe::Dupe; +use pyrefly_derive::TypeEq; +use pyrefly_python::module::Module; +use pyrefly_python::nesting_context::NestingContext; +use pyrefly_python::qname::QName; +use pyrefly_util::arc_id::ArcId; +use pyrefly_util::visit::Visit; +use pyrefly_util::visit::VisitMut; +use ruff_python_ast::Identifier; + +use crate::equality::TypeEq; +use crate::equality::TypeEqCtx; +use crate::heap::TypeHeap; +use crate::types::Type; + +/// Used to represent Sentinel calls. Each Sentinel is unique, so use the ArcId to separate them. +#[derive(Clone, Dupe, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct Sentinel(ArcId); + +// This is a lie, we do have types in the bound position +impl Visit for Sentinel { + const RECURSE_CONTAINS: bool = false; + fn recurse<'a>(&'a self, _: &mut dyn FnMut(&'a Type)) {} +} + +impl VisitMut for Sentinel { + const RECURSE_CONTAINS: bool = false; + fn recurse_mut(&mut self, _: &mut dyn FnMut(&mut Type)) {} +} + +impl Display for Sentinel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.qname.id()) + } +} + +#[derive(Debug, PartialEq, TypeEq, Eq, Ord, PartialOrd)] +struct SentinelInner { + qname: QName, +} + +impl Sentinel { + pub fn new(name: Identifier, module: Module) -> Self { + Self(ArcId::new(SentinelInner { + // TODO: properly take parent from caller of new() + qname: QName::new(name, NestingContext::toplevel(), module), + })) + } + + pub fn qname(&self) -> &QName { + &self.0.qname + } + + pub fn to_type(&self, heap: &TypeHeap) -> Type { + heap.mk_sentinel(self.dupe()) + } + + pub fn type_eq_inner(&self, other: &Self, ctx: &mut TypeEqCtx) -> bool { + self.0.type_eq(&other.0, ctx) + } +} diff --git a/crates/pyrefly_types/src/stdlib.rs b/crates/pyrefly_types/src/stdlib.rs index b44209c09a..0af5273372 100644 --- a/crates/pyrefly_types/src/stdlib.rs +++ b/crates/pyrefly_types/src/stdlib.rs @@ -105,6 +105,8 @@ pub struct Stdlib { /// After 3.14, `typing_extensions` reexports from `typing`. /// For 3.12 and 3.13 defined separately in both locations. type_alias_type: StdlibResult, + /// Defined in `typing_extensions` until 3.15, defined in `typing` since 3.15. + sentinel: StdlibResult, traceback_type: StdlibResult, builtins_type: StdlibResult, /// Introduced in Python 3.10. @@ -257,6 +259,7 @@ impl Stdlib { param_spec_kwargs: lookup_concrete(standardised(3, 10), "ParamSpecKwargs"), type_var_tuple: lookup_concrete(standardised(3, 11), "TypeVarTuple"), type_alias_type: lookup_concrete(standardised(3, 12), "TypeAliasType"), + sentinel: lookup_concrete(standardised(3, 15), "Sentinel"), traceback_type: lookup_concrete(types, "TracebackType"), function_type: lookup_concrete(types, "FunctionType"), method_type: lookup_concrete(types, "MethodType"), @@ -581,6 +584,10 @@ impl Stdlib { Self::primitive(&self.type_alias_type) } + pub fn sentinel(&self) -> &ClassType { + Self::primitive(&self.sentinel) + } + pub fn traceback_type(&self) -> &ClassType { Self::primitive(&self.traceback_type) } diff --git a/crates/pyrefly_types/src/types.rs b/crates/pyrefly_types/src/types.rs index 54d64576fd..e3cc558bcc 100644 --- a/crates/pyrefly_types/src/types.rs +++ b/crates/pyrefly_types/src/types.rs @@ -59,6 +59,7 @@ use crate::literal::Literal; use crate::module::ModuleType; use crate::param_spec::ParamSpec; use crate::quantified::Quantified; +use crate::sentinel::Sentinel; use crate::shaped_array::ShapedArrayType; use crate::simplify::unions; use crate::special_form::SpecialForm; @@ -858,6 +859,9 @@ pub enum Type { /// be immediately looked up for untyping (see `TypeAliasData::TypeAliasRef`), `UntypedAlias` /// stores a reference that is untyped once we actually look up the value. UntypedAlias(Box), + // Sentinel types, documented here: https://docs.python.org/3.15/library/functions.html#sentinel + // First introduced in PEP 661: https://peps.python.org/pep-0661/ + Sentinel(Sentinel), /// Represents the result of a super() call. The first ClassType is the point in the MRO that attribute lookup /// on the super instance should start at (*not* the class passed to the super() call), and the second /// ClassType is the second argument (implicit or explicit) to the super() call. For example, in: @@ -919,6 +923,7 @@ impl Visit for Type { Type::Annotated(x, _metadata) => x.visit(f), Type::Unpack(x) => x.visit(f), Type::TypeVar(x) => x.visit(f), + Type::Sentinel(x) => x.visit(f), Type::ParamSpec(x) => x.visit(f), Type::TypeVarTuple(x) => x.visit(f), Type::SpecialForm(x) => x.visit(f), @@ -975,6 +980,7 @@ impl VisitMut for Type { Type::Annotated(x, _metadata) => x.visit_mut(f), Type::Unpack(x) => x.visit_mut(f), Type::TypeVar(x) => x.visit_mut(f), + Type::Sentinel(x) => x.visit_mut(f), Type::ParamSpec(x) => x.visit_mut(f), Type::TypeVarTuple(x) => x.visit_mut(f), Type::SpecialForm(x) => x.visit_mut(f), @@ -1919,6 +1925,7 @@ impl Type { Type::ParamSpec(t) => Some(t.qname()), Type::SelfType(cls) => Some(cls.qname()), Type::Literal(lit) if let Lit::Enum(e) = &lit.value => Some(e.class.qname()), + Type::Sentinel(s) => Some(s.qname()), _ => None, } } @@ -1932,6 +1939,7 @@ impl Type { Type::Literal(lit) if let Lit::Str(x) = &lit.value => Some(!x.is_empty()), Type::Type(_) => Some(true), Type::None => Some(false), + Type::Sentinel(_) => Some(true), Type::Tuple(Tuple::Concrete(elements)) => Some(!elements.is_empty()), Type::Union(u) => { let mut answer = None; diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 406051aafb..2c0eeec0a2 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -2336,6 +2336,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { Type::TypeVar(_) => acc.push(AttributeBase1::ClassInstance( self.stdlib.type_var().clone(), )), + Type::Sentinel(_) => acc.push(AttributeBase1::ClassInstance( + self.stdlib.sentinel().clone(), + )), Type::ParamSpec(_) => acc.push(AttributeBase1::ClassInstance( self.stdlib.param_spec().clone(), )), diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 1a16a14bc3..e6e095cb9f 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -95,6 +95,7 @@ use crate::types::literal::Lit; use crate::types::param_spec::ParamSpec; use crate::types::quantified::Quantified; use crate::types::quantified::QuantifiedKind; +use crate::types::sentinel::Sentinel; use crate::types::special_form::SpecialForm; use crate::types::tuple::Tuple; use crate::types::type_info::TypeInfo; @@ -1788,6 +1789,97 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } + pub fn sentinel_from_call( + &self, + name: Identifier, + x: &ExprCall, + errors: &ErrorCollector, + ) -> Sentinel { + let mut iargs = x.arguments.args.iter(); + if let Some(arg) = iargs.next() { + if let Expr::StringLiteral(lit) = arg { + if lit.value.to_str() != name.id.as_str() { + self.error( + errors, + x.range, + ErrorKind::InvalidSentinel, + format!( + "Sentinel must be assigned to a variable named `{}`", + lit.value.to_str() + ), + ); + } + } else { + self.error( + errors, + arg.range(), + ErrorKind::InvalidSentinel, + "Expected first argument of Sentinel to be a string literal".to_owned(), + ); + } + } else { + self.error( + errors, + x.range, + ErrorKind::InvalidSentinel, + "Sentinel requires a name as the first argument".to_owned(), + ); + } + if let Some(arg) = iargs.next() { + let args_range_end = x.arguments.args.last().map(|arg| arg.range().end()); + let range = TextRange::new( + arg.range().start(), + // args_range_end should never be None as it should only be None if there are + // no args, but no reason not to have a default here anyway. + args_range_end.unwrap_or_else(|| arg.range().end()), + ); + self.error( + errors, + range, + ErrorKind::InvalidSentinel, + "Sentinel only takes one positional argument".to_owned(), + ); + } + + for kw in &x.arguments.keywords { + match &kw.arg { + Some(id) => match id.id.as_str() { + "repr" => { + let got = self.expr_infer(&kw.value, errors); + if !self + .is_subset_eq(&got, &self.heap.mk_class_type(self.stdlib.str().clone())) + { + self.error( + errors, + kw.range, + ErrorKind::InvalidSentinel, + format!("Invalid type for Sentinel `repr` {got}"), + ); + } + } + _ => { + self.error( + errors, + kw.range, + ErrorKind::InvalidSentinel, + format!("Unexpected keyword argument `{}` to Sentinel", id.id), + ); + } + }, + _ => { + self.error( + errors, + kw.range, + ErrorKind::InvalidSentinel, + "Cannot pass unpacked keyword arguments to Sentinel".to_owned(), + ); + } + } + } + + Sentinel::new(name, self.module().dupe()) + } + pub fn typevar_from_call( &self, name: Identifier, diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index b3e380b396..b5a7c89217 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -294,6 +294,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { { self.heap.mk_never() } + (Type::Sentinel(s1), Type::Sentinel(s2)) if s1 == s2 => self.heap.mk_never(), (Type::ClassType(cls), Type::Literal(lit)) if cls.is_builtin("bool") && let Lit::Bool(b) = &lit.value => @@ -850,7 +851,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { range, ); match right { - Type::None | Type::Ellipsis | Type::Literal(_) => { + Type::None | Type::Ellipsis | Type::Literal(_) | Type::Sentinel(_) => { if self.is_subset_eq(&right, &facet_ty) { t.clone() } else { @@ -872,8 +873,8 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { ); match (&facet_ty, &right) { ( - Type::None | Type::Ellipsis | Type::Literal(_), - Type::None | Type::Ellipsis | Type::Literal(_), + Type::None | Type::Ellipsis | Type::Literal(_) | Type::Sentinel(_), + Type::None | Type::Ellipsis | Type::Literal(_) | Type::Sentinel(_), ) if self.literal_equal(&right, &facet_ty) => self.heap.mk_never(), _ => t.clone(), } @@ -1410,7 +1411,10 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } AtomicNarrowOp::Eq(v) => { let right = self.expr_infer(v, errors); - if matches!(right, Type::Literal(_) | Type::None | Type::Ellipsis) { + if matches!( + right, + Type::Literal(_) | Type::None | Type::Ellipsis | Type::Sentinel(_) + ) { self.intersect(ty, &right) } else { ty.clone() @@ -1418,7 +1422,10 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } AtomicNarrowOp::NotEq(v) => { let right = self.expr_infer(v, errors); - if matches!(right, Type::Literal(_) | Type::None | Type::Ellipsis) { + if matches!( + right, + Type::Literal(_) | Type::None | Type::Ellipsis | Type::Sentinel(_) + ) { self.distribute_over_union(ty, |t| match (t, &right) { (_, _) if self.literal_equal(t, &right) => self.heap.mk_never(), (Type::ClassType(cls), Type::Literal(lit)) @@ -2163,6 +2170,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { match (left, right) { (Type::None, Type::None) => true, (Type::Ellipsis, Type::Ellipsis) => true, + (Type::Sentinel(s1), Type::Sentinel(s2)) => s1 == s2, (Type::Literal(left), Type::Literal(right)) => left.value == right.value, _ => false, } diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index ff96726dfc..41fef1134e 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -5343,6 +5343,28 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.heap.mk_none() } Binding::Delete(x) => self.check_del_statement(x, errors), + Binding::Sentinel(x) => { + let (ann, name, call) = x.as_ref(); + let ty = self + .sentinel_from_call(name.clone(), call, errors) + .to_type(self.heap); + if let Some(k) = ann + && let AnnotationWithTarget { + target, + annotation: + Annotation { + ty: Some(want), + qualifiers: _, + }, + } = &*self.get_idx(*k) + { + // Validate the annotation already on assigned name + self.check_type(&ty, want, call.range, errors, &|| { + TypeCheckContext::of_kind(TypeCheckKind::from_annotation_target(target)) + }); + } + ty + } } } @@ -5697,6 +5719,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } Some(*t) } + Type::Sentinel(sentinel) => Some(self.heap.mk_sentinel(sentinel)), Type::None => Some(self.heap.mk_none()), // Both a value and a type Type::Ellipsis => Some(self.heap.mk_ellipsis()), // A bit weird because of tuples, so just promote it Type::Any(style) => Some(style.propagate()), diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index 8df403e666..4709b2493a 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -2354,6 +2354,7 @@ pub enum Binding { /// A match statement or if/elif chain that may be type-exhaustive. /// Resolves to Never if ANY narrow entry narrows to Never, None otherwise. Exhaustive(Box), + Sentinel(Box<(Option>, Identifier, Box)>), } impl DisplayWith for Binding { @@ -2396,6 +2397,10 @@ impl DisplayWith for Binding { let (a, name, call) = x.as_ref(); write!(f, "TypeVarTuple({}, {name}, {})", ann(a), m.display(call)) } + Self::Sentinel(x) => { + let (a, name, call) = x.as_ref(); + write!(f, "Sentinel({}, {name}, {})", ann(a), m.display(call)) + } Self::ReturnExplicit(x) => { write!(f, "ReturnExplicit({}, ", ann(&x.annot))?; match &x.expr { @@ -2693,6 +2698,7 @@ impl Binding { Binding::NameAssign(x) if x.name.as_str() == x.name.to_uppercase() => { Some(SymbolKind::Constant) } + Binding::Sentinel(_) => Some(SymbolKind::Constant), Binding::NameAssign(x) => { if x.name .as_str() diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 26ab6ea594..1e26f1b8ce 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -281,6 +281,30 @@ impl<'a> BindingsBuilder<'a> { }) } + fn assign_sentinel(&mut self, name: &ExprName, call: &mut ExprCall) { + // Type var declarations are static types only; skip them for first-usage type inference. + let static_type_usage = &mut Usage::StaticTypeInformation { + is_annotation: false, + }; + self.ensure_expr(&mut call.func, static_type_usage); + if let Some(expr) = call.arguments.args.iter_mut().next() { + self.ensure_expr(expr, static_type_usage); + } + for kw in call.arguments.keywords.iter_mut() { + self.ensure_expr(&mut kw.value, static_type_usage); + } + // Like legacy type var, Sentinel can only be created with a single Sentinel binding to a + // single variable (https://peps.python.org/pep-0661/#typing). Thus we bind it in the same + // way legacy type vars are bound. + self.bind_legacy_type_var_or_typing_alias(name, |ann| { + Binding::Sentinel(Box::new(( + ann, + Ast::expr_name_identifier(name.clone()), + Box::new(call.clone()), + ))) + }) + } + fn ensure_type_alias_type_args( &mut self, call: &mut ExprCall, @@ -598,6 +622,10 @@ impl<'a> BindingsBuilder<'a> { self.assign_type_var_tuple(name, call); return; } + SpecialExport::Sentinel => { + self.assign_sentinel(name, call); + return; + } SpecialExport::Enum | SpecialExport::IntEnum | SpecialExport::StrEnum => { diff --git a/pyrefly/lib/export/special.rs b/pyrefly/lib/export/special.rs index 037b3a79c6..630416a0cb 100644 --- a/pyrefly/lib/export/special.rs +++ b/pyrefly/lib/export/special.rs @@ -74,6 +74,7 @@ pub enum SpecialExport { UsesShapeDsl, ShapeDslFunction, ShapedArray, + Sentinel, } impl SpecialExport { @@ -139,6 +140,7 @@ impl SpecialExport { "uses_shape_dsl" => Some(Self::UsesShapeDsl), "shape_dsl_function" => Some(Self::ShapeDslFunction), "shaped_array" => Some(Self::ShapedArray), + "Sentinel" => Some(Self::Sentinel), _ => None, } } @@ -213,6 +215,7 @@ impl SpecialExport { Self::UsesShapeDsl => matches!(m.as_str(), "shape_extensions"), Self::ShapeDslFunction => matches!(m.as_str(), "shape_extensions.dsl"), Self::ShapedArray => matches!(m.as_str(), "shape_extensions"), + Self::Sentinel => matches!(m.as_str(), "typing_extensions"), } } diff --git a/pyrefly/lib/query.rs b/pyrefly/lib/query.rs index 3225a06c01..bef25d15b8 100644 --- a/pyrefly/lib/query.rs +++ b/pyrefly/lib/query.rs @@ -425,6 +425,10 @@ fn type_shape_kind(context: &TypeShapeContext, ty: &Type) -> TypeShapeKind { "typing.Literal", vec![named_leaf(literal.value.to_string())], ), + Type::Sentinel(sentinel) => named_type_shape_kind( + "typing_extensions.Sentinel", + vec![named_leaf(format!("{}", sentinel))], + ), Type::LiteralString(_) => { named_type_shape_kind("typing_extensions.LiteralString", Vec::new()) } diff --git a/pyrefly/lib/report/cinderx/convert.rs b/pyrefly/lib/report/cinderx/convert.rs index 91cd8c3449..1b04e58d74 100644 --- a/pyrefly/lib/report/cinderx/convert.rs +++ b/pyrefly/lib/report/cinderx/convert.rs @@ -432,6 +432,7 @@ pub(crate) fn type_to_structured( | Type::ParamSpec(_) | Type::TypeVarTuple(_) | Type::TypeForm(_) + | Type::Sentinel(_) | Type::ElementOfTypeVarTuple(_) | Type::ShapedArray(_) | Type::NNModule(_) diff --git a/pyrefly/lib/test/mod.rs b/pyrefly/lib/test/mod.rs index 7f9cc219bc..07bb498e0c 100644 --- a/pyrefly/lib/test/mod.rs +++ b/pyrefly/lib/test/mod.rs @@ -67,6 +67,7 @@ mod redundant_cast; mod returns; mod scope; mod semantic_syntax_errors; +mod sentinel; mod shape_dsl; mod simple; mod slots; diff --git a/pyrefly/lib/test/narrow.rs b/pyrefly/lib/test/narrow.rs index 02fba29413..c28f133ad8 100644 --- a/pyrefly/lib/test/narrow.rs +++ b/pyrefly/lib/test/narrow.rs @@ -3230,3 +3230,90 @@ def b(): assert_type(val, int) "#, ); + +testcase!( + test_sentinel_type_narrow_to_boolean, + r#" +from typing_extensions import Sentinel +from typing import Literal, assert_type + +MISSING = Sentinel("MISSING") +def f(x: bool | MISSING): + if x is MISSING: + assert_type(x, MISSING) + else: + assert_type(x, bool) + if x is not MISSING: + assert_type(x, bool) + else: + assert_type(x, MISSING) + "#, +); + +testcase!( + test_sentinel_type_narrow_to_boolean_eq, + r#" +from typing_extensions import Sentinel +from typing import Literal, assert_type + +MISSING = Sentinel("MISSING") +def f(x: bool | MISSING): + if x == MISSING: + assert_type(x, MISSING) + else: + assert_type(x, bool) + if x != MISSING: + assert_type(x, bool) + else: + assert_type(x, MISSING) + "#, +); + +testcase!( + sentinel_narrow_from_union, + r#" +from typing_extensions import Sentinel +from typing import Literal, assert_type + +MISSING = Sentinel("MISSING") +def f(x: int | bool | MISSING): + if x is MISSING: + assert_type(x, MISSING) + else: + assert_type(x, int | bool) + if x is not MISSING: + assert_type(x, int | bool) + else: + assert_type(x, MISSING) + "#, +); + +testcase!( + sentinel_is_truthy, + r#" +from typing_extensions import Sentinel +from typing import Literal, assert_type + +MISSING = Sentinel("MISSING") +x: MISSING | None = MISSING +if x: + assert_type(x, MISSING) +else: + assert_type(x, None) + "#, +); + +testcase!( + sentinel_narrow_with_type_param, + r#" +from typing_extensions import Sentinel + +MIS = Sentinel("MIS", repr="MISSING") + +def a[T](x: T | MIS) -> T: + if x is not MIS: + return x + else: + raise ValueError("a") + "#, +); diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs new file mode 100644 index 0000000000..b142d075eb --- /dev/null +++ b/pyrefly/lib/test/sentinel.rs @@ -0,0 +1,122 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use crate::testcase; + +testcase!( + test_sentinel_construction_success, + r#" +from typing_extensions import Sentinel + +A = Sentinel("A") + "#, +); + +testcase!( + test_sentinel_construction_second_positional_arg, + r#" +from typing_extensions import Sentinel + +A = Sentinel("A", 123) # E: Sentinel only takes one positional argument + "#, +); + +testcase!( + test_sentinel_construction_with_repr_success, + r#" +from typing_extensions import Sentinel + +A = Sentinel("A", repr="some text") + "#, +); + +testcase!( + test_sentinel_construction_with_repr_str_success, + r#" +from typing_extensions import Sentinel + +text: str = "some other text" +A = Sentinel("A", repr=text) + "#, +); + +testcase!( + test_sentinel_construction_with_repr_int_error, + r#" +from typing_extensions import Sentinel + +text = 5 +A = Sentinel("A", repr=text) # E: Invalid type for Sentinel `repr` Literal[5] + "#, +); + +testcase!( + test_sentinel_construction_name_kwarg_error, + r#" +from typing_extensions import Sentinel + +A = Sentinel(name="A") # E: Sentinel requires a name as the first argument # E: Unexpected keyword argument `name` to Sentinel + "#, +); + +testcase!( + test_sentinel_construction_different_names, + r#" +from typing_extensions import Sentinel + +A = Sentinel("B") # E: Sentinel must be assigned to a variable named `B` + "#, +); + +testcase!( + test_sentinel_typehint_success, + r#" +from typing_extensions import Sentinel + +A: A = Sentinel("A") + "#, +); + +testcase!( + test_sentinel_typehint_different_sentinel, + r#" +from typing_extensions import Sentinel + +A = Sentinel("A") +B: A = Sentinel("B") # E: `Sentinel` is not assignable to `A` + "#, +); + +testcase!( + test_sentinel_typehint_any, + r#" +from typing import Any, assert_type +from typing_extensions import Sentinel + +A: Any = Sentinel("A") +assert_type(A, Any) + "#, +); + +testcase!( + test_sentinel_violates_annotation, + r#" +from typing_extensions import Sentinel + +MISSING: int = 0 +MISSING = Sentinel('MISSING') # E: `MISSING` is not assignable to variable `MISSING` with type `int` + "#, +); + +testcase!( + test_sentinel_no_args_error, + r#" +from typing_extensions import Sentinel + +MISSING = Sentinel() # E: Sentinel requires a name as the first argument + "#, +); diff --git a/pyrefly/lib/tsp/type_conversion.rs b/pyrefly/lib/tsp/type_conversion.rs index 8b261bc7ea..cb0891dd6d 100644 --- a/pyrefly/lib/tsp/type_conversion.rs +++ b/pyrefly/lib/tsp/type_conversion.rs @@ -360,6 +360,9 @@ impl TypeConverter<'_> { // --- Materialization is a solver artifact --- PyreflyType::Materialization => builtin("Unknown"), + + // --- Sentinel type --- + PyreflyType::Sentinel(_) => builtin("Sentinel"), } } diff --git a/website/docs/error-kinds.mdx b/website/docs/error-kinds.mdx index 8930748c3f..57c3587005 100644 --- a/website/docs/error-kinds.mdx +++ b/website/docs/error-kinds.mdx @@ -885,6 +885,20 @@ class TD(TypedDict): x: Option[Self] ``` +## invalid-sentinel + +An error caused by incorrect definition of a Sentinel. A few examples: + +```python +from typing_extensions import Sentinel + +# Sentinels must be assigned to a matching variable. +A = Sentinel("B") + +# Invalid arguments passed to Sentinel constructor +MISSING = Sentinel("MISSING", non_existent="") +``` + ## invalid-super-call `super()` has [a few restrictions](https://docs.python.org/3/library/functions.html#super) on how it is called. From 2553533ca9f80e04666f1047a86ed5ce346c3fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:27:32 +0000 Subject: [PATCH 02/11] BuiltinSentinel working --- crates/pyrefly_types/src/stdlib.rs | 9 +++++-- pyrefly/lib/binding/stmt.rs | 4 +++ pyrefly/lib/export/special.rs | 3 +++ pyrefly/lib/test/sentinel.rs | 42 ++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/pyrefly_types/src/stdlib.rs b/crates/pyrefly_types/src/stdlib.rs index 0af5273372..3aa4b7011e 100644 --- a/crates/pyrefly_types/src/stdlib.rs +++ b/crates/pyrefly_types/src/stdlib.rs @@ -105,7 +105,8 @@ pub struct Stdlib { /// After 3.14, `typing_extensions` reexports from `typing`. /// For 3.12 and 3.13 defined separately in both locations. type_alias_type: StdlibResult, - /// Defined in `typing_extensions` until 3.15, defined in `typing` since 3.15. + /// Defined in `typing_extensions` as Sentinel. + /// Defined in `builtins` as `sentinel` since 3.15. sentinel: StdlibResult, traceback_type: StdlibResult, builtins_type: StdlibResult, @@ -259,7 +260,11 @@ impl Stdlib { param_spec_kwargs: lookup_concrete(standardised(3, 10), "ParamSpecKwargs"), type_var_tuple: lookup_concrete(standardised(3, 11), "TypeVarTuple"), type_alias_type: lookup_concrete(standardised(3, 12), "TypeAliasType"), - sentinel: lookup_concrete(standardised(3, 15), "Sentinel"), + // sentinel: lookup_concrete(typing_extensions, "Sentinel"), + sentinel: version + .at_least(3, 15) + .then(|| lookup_concrete(builtins, "sentinel")) + .unwrap_or_else(|| lookup_concrete(typing_extensions, "Sentinel")), traceback_type: lookup_concrete(types, "TracebackType"), function_type: lookup_concrete(types, "FunctionType"), method_type: lookup_concrete(types, "MethodType"), diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 1e26f1b8ce..7f9a59b62a 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -626,6 +626,10 @@ impl<'a> BindingsBuilder<'a> { self.assign_sentinel(name, call); return; } + SpecialExport::BuiltinsSentinel => { + self.assign_sentinel(name, call); + return; + } SpecialExport::Enum | SpecialExport::IntEnum | SpecialExport::StrEnum => { diff --git a/pyrefly/lib/export/special.rs b/pyrefly/lib/export/special.rs index 630416a0cb..39284d7cc1 100644 --- a/pyrefly/lib/export/special.rs +++ b/pyrefly/lib/export/special.rs @@ -75,6 +75,7 @@ pub enum SpecialExport { ShapeDslFunction, ShapedArray, Sentinel, + BuiltinsSentinel, } impl SpecialExport { @@ -141,6 +142,7 @@ impl SpecialExport { "shape_dsl_function" => Some(Self::ShapeDslFunction), "shaped_array" => Some(Self::ShapedArray), "Sentinel" => Some(Self::Sentinel), + "sentinel" => Some(Self::BuiltinsSentinel), _ => None, } } @@ -216,6 +218,7 @@ impl SpecialExport { Self::ShapeDslFunction => matches!(m.as_str(), "shape_extensions.dsl"), Self::ShapedArray => matches!(m.as_str(), "shape_extensions"), Self::Sentinel => matches!(m.as_str(), "typing_extensions"), + Self::BuiltinsSentinel => matches!(m.as_str(), "builtins"), } } diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index b142d075eb..6c53627d81 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -5,6 +5,9 @@ * LICENSE file in the root directory of this source tree. */ +use pyrefly_python::sys_info::PythonVersion; + +use crate::test::util::TestEnv; use crate::testcase; testcase!( @@ -120,3 +123,42 @@ from typing_extensions import Sentinel MISSING = Sentinel() # E: Sentinel requires a name as the first argument "#, ); + +testcase!( + test_sentinel_typing_extensions_3_15, + TestEnv::new().with_version(PythonVersion::new(3, 15, 0)), + r#" +from typing import Literal, assert_type +from typing_extensions import Sentinel + +A = Sentinel("A") +def foo(a: A | Literal[False]): + if a: + assert_type(a, A) + else: + assert_type(a, Literal[False]) + "#, +); + +testcase!( + test_sentinel_lowercase_3_15, + TestEnv::new().with_version(PythonVersion::new(3, 15, 0)), + r#" +from typing import Literal, assert_type + +A = sentinel("A") +def foo(a: A | Literal[False]): + if a: + assert_type(a, A) + else: + assert_type(a, Literal[False]) + "#, +); + +testcase!( + test_sentinel_lowercase_3_14_error, + TestEnv::new().with_version(PythonVersion::new(3, 14, 0)), + r#" +A = sentinel("A") # E: Could not find name `sentinel` + "#, +); From d1ce26f21c3032125aa47e3fd5ffc588b8cd33d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:29:53 +0000 Subject: [PATCH 03/11] Rename Sentinel to lower case in error messages --- pyrefly/lib/alt/expr.rs | 8 ++++---- pyrefly/lib/test/sentinel.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index e6e095cb9f..e046ee102c 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -1814,7 +1814,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { errors, arg.range(), ErrorKind::InvalidSentinel, - "Expected first argument of Sentinel to be a string literal".to_owned(), + "Expected first argument of sentinel to be a string literal".to_owned(), ); } } else { @@ -1853,7 +1853,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { errors, kw.range, ErrorKind::InvalidSentinel, - format!("Invalid type for Sentinel `repr` {got}"), + format!("Invalid type for sentinel `repr` {got}"), ); } } @@ -1862,7 +1862,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { errors, kw.range, ErrorKind::InvalidSentinel, - format!("Unexpected keyword argument `{}` to Sentinel", id.id), + format!("Unexpected keyword argument `{}` to sentinel", id.id), ); } }, @@ -1871,7 +1871,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { errors, kw.range, ErrorKind::InvalidSentinel, - "Cannot pass unpacked keyword arguments to Sentinel".to_owned(), + "Cannot pass unpacked keyword arguments to sentinel".to_owned(), ); } } diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index 6c53627d81..ca385abe8f 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -53,7 +53,7 @@ testcase!( from typing_extensions import Sentinel text = 5 -A = Sentinel("A", repr=text) # E: Invalid type for Sentinel `repr` Literal[5] +A = Sentinel("A", repr=text) # E: Invalid type for sentinel `repr` Literal[5] "#, ); @@ -62,7 +62,7 @@ testcase!( r#" from typing_extensions import Sentinel -A = Sentinel(name="A") # E: Sentinel requires a name as the first argument # E: Unexpected keyword argument `name` to Sentinel +A = Sentinel(name="A") # E: Sentinel requires a name as the first argument # E: Unexpected keyword argument `name` to sentinel "#, ); From 4847feb5c95ee8388b6519b2d0870f1327175949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:16:02 +0000 Subject: [PATCH 04/11] Allow assigning sentinel to variable with different name This seems to be what is being decided in the python typing spec, see the PR description: https://github.com/python/typing/pull/2277 Although some typecheckers, like pyright, still have the old behaviour recommended in the PEP of giving an error when a different name is assigned. --- pyrefly/lib/alt/expr.rs | 17 ++++------------- pyrefly/lib/test/sentinel.rs | 28 ++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index e046ee102c..6812f7f9ee 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -1791,24 +1791,15 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { pub fn sentinel_from_call( &self, - name: Identifier, + assignment_name: Identifier, x: &ExprCall, errors: &ErrorCollector, ) -> Sentinel { + let mut sentinel_name = assignment_name; let mut iargs = x.arguments.args.iter(); if let Some(arg) = iargs.next() { if let Expr::StringLiteral(lit) = arg { - if lit.value.to_str() != name.id.as_str() { - self.error( - errors, - x.range, - ErrorKind::InvalidSentinel, - format!( - "Sentinel must be assigned to a variable named `{}`", - lit.value.to_str() - ), - ); - } + sentinel_name = Identifier::new(lit.value.to_str(), lit.range()); } else { self.error( errors, @@ -1877,7 +1868,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } - Sentinel::new(name, self.module().dupe()) + Sentinel::new(sentinel_name, self.module().dupe()) } pub fn typevar_from_call( diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index ca385abe8f..bbcc123027 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -67,11 +67,35 @@ A = Sentinel(name="A") # E: Sentinel requires a name as the first argument # E: ); testcase!( - test_sentinel_construction_different_names, + test_sentinel_construction_different_names_allowed, r#" from typing_extensions import Sentinel -A = Sentinel("B") # E: Sentinel must be assigned to a variable named `B` +A = Sentinel("") + "#, +); + +testcase!( + test_sentinel_uses_sentinel_string_literal_name_in_error_messages, + r#" +from typing_extensions import Sentinel + +A = Sentinel("") + +def foo(a: A): + b: int = a # E: `` is not assignable to `int` + "#, +); + +testcase!( + test_sentinel_defaults_to_assignment_name_if_not_constructed_with_name, + r#" +from typing_extensions import Sentinel + +A = Sentinel() # E: Sentinel requires a name as the first argument + +def foo(a: A): + b: int = a # E: `A` is not assignable to `int` "#, ); From 153d58aacb30f7425ab9273a67b1e93abc6d550c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:06:16 +0000 Subject: [PATCH 05/11] Add test to confirm sentinel in class body working --- pyrefly/lib/test/sentinel.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index bbcc123027..b7ea724146 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -186,3 +186,20 @@ testcase!( A = sentinel("A") # E: Could not find name `sentinel` "#, ); + +testcase!( + test_sentinel_in_class_body, + r#" +from typing import assert_type +from typing_extensions import Sentinel + +class Cls: + IN_CLASS = Sentinel("Cls.IN_CLASS") + +def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None: + if x is Cls.IN_CLASS: + assert_type(x, Cls.IN_CLASS) + else: + assert_type(x, int) + "#, +); From b66942c9d21d4892a591c4d7d460fa109ea36a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:27:01 +0000 Subject: [PATCH 06/11] Differentiate between typing_extensions Sentinel and builtins sentinel This is necessary as some properties, like __name__, are only available on the builtin sentinel, not the typing_extensions Sentienl type. This can be confirmed by running the following: ``` uv run -p 3.15 --with 'typing_extensions' python -c 'from typing_extensions import Sentinel; MISSING = Sentinel("MISSING"); print(MISSING.__name__)' > Traceback (most recent call last): > File "", line 1, in > from typing_extensions import Sentinel; MISSING = Sentinel("MISSING"); MISSING.__name__ > ^^^^^^^^^^^^^^^^ > AttributeError: 'Sentinel' object has no attribute '__name__'. Did you mean '.__ne__' instead of '.__name__'? uv run -p 3.15 --with 'typing_extensions' python -c 'MISSING = sentinel("MISSING"); print(MISSING.__name__)' > MISSING ``` --- crates/pyrefly_types/src/sentinel.rs | 23 ++++++++++++++++++++- crates/pyrefly_types/src/stdlib.rs | 18 +++++++++------- pyrefly/lib/alt/attr.rs | 12 ++++++++--- pyrefly/lib/alt/expr.rs | 4 +++- pyrefly/lib/alt/solve.rs | 4 ++-- pyrefly/lib/binding/binding.rs | 21 ++++++++++++++++--- pyrefly/lib/binding/stmt.rs | 13 +++++++++--- pyrefly/lib/test/sentinel.rs | 31 ++++++++++++++++++++++++++++ 8 files changed, 106 insertions(+), 20 deletions(-) diff --git a/crates/pyrefly_types/src/sentinel.rs b/crates/pyrefly_types/src/sentinel.rs index 4f11410b79..158c53ee9d 100644 --- a/crates/pyrefly_types/src/sentinel.rs +++ b/crates/pyrefly_types/src/sentinel.rs @@ -24,6 +24,21 @@ use crate::equality::TypeEqCtx; use crate::heap::TypeHeap; use crate::types::Type; +#[derive(Clone, Copy, Dupe, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, TypeEq)] +pub enum SentinelKind { + Builtins, + TypingExtensions, +} + +impl SentinelKind { + pub fn name(&self) -> &str { + match self { + Self::Builtins => "sentinel", + Self::TypingExtensions => "Sentinel", + } + } +} + /// Used to represent Sentinel calls. Each Sentinel is unique, so use the ArcId to separate them. #[derive(Clone, Dupe, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct Sentinel(ArcId); @@ -48,16 +63,22 @@ impl Display for Sentinel { #[derive(Debug, PartialEq, TypeEq, Eq, Ord, PartialOrd)] struct SentinelInner { qname: QName, + kind: SentinelKind, } impl Sentinel { - pub fn new(name: Identifier, module: Module) -> Self { + pub fn new(name: Identifier, module: Module, kind: SentinelKind) -> Self { Self(ArcId::new(SentinelInner { + kind, // TODO: properly take parent from caller of new() qname: QName::new(name, NestingContext::toplevel(), module), })) } + pub fn kind(&self) -> SentinelKind { + self.0.kind + } + pub fn qname(&self) -> &QName { &self.0.qname } diff --git a/crates/pyrefly_types/src/stdlib.rs b/crates/pyrefly_types/src/stdlib.rs index 3aa4b7011e..e73c8dd06a 100644 --- a/crates/pyrefly_types/src/stdlib.rs +++ b/crates/pyrefly_types/src/stdlib.rs @@ -106,8 +106,11 @@ pub struct Stdlib { /// For 3.12 and 3.13 defined separately in both locations. type_alias_type: StdlibResult, /// Defined in `typing_extensions` as Sentinel. + /// Different attributes from `builtins` sentinel. + sentinel_typing_extensions: StdlibResult, /// Defined in `builtins` as `sentinel` since 3.15. - sentinel: StdlibResult, + /// Different attributes from `typing_extensions` Sentinel. + sentinel_builtin: StdlibResult, traceback_type: StdlibResult, builtins_type: StdlibResult, /// Introduced in Python 3.10. @@ -261,10 +264,8 @@ impl Stdlib { type_var_tuple: lookup_concrete(standardised(3, 11), "TypeVarTuple"), type_alias_type: lookup_concrete(standardised(3, 12), "TypeAliasType"), // sentinel: lookup_concrete(typing_extensions, "Sentinel"), - sentinel: version - .at_least(3, 15) - .then(|| lookup_concrete(builtins, "sentinel")) - .unwrap_or_else(|| lookup_concrete(typing_extensions, "Sentinel")), + sentinel_builtin: lookup_concrete(builtins, "sentinel"), + sentinel_typing_extensions: lookup_concrete(typing_extensions, "Sentinel"), traceback_type: lookup_concrete(types, "TracebackType"), function_type: lookup_concrete(types, "FunctionType"), method_type: lookup_concrete(types, "MethodType"), @@ -589,8 +590,11 @@ impl Stdlib { Self::primitive(&self.type_alias_type) } - pub fn sentinel(&self) -> &ClassType { - Self::primitive(&self.sentinel) + pub fn sentinel_builtin(&self) -> &ClassType { + Self::primitive(&self.sentinel_builtin) + } + pub fn sentinel_typing_extensions(&self) -> &ClassType { + Self::primitive(&self.sentinel_typing_extensions) } pub fn traceback_type(&self) -> &ClassType { diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 2c0eeec0a2..d97acc524c 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -14,6 +14,7 @@ use pyrefly_types::dimension::SizeExpr; use pyrefly_types::heap::TypeHeap; use pyrefly_types::lit_int::LitInt; use pyrefly_types::literal::LitEnum; +use pyrefly_types::sentinel::SentinelKind; use pyrefly_types::shaped_array::ShapedArrayShape; use pyrefly_types::shaped_array::ShapedArrayType; use pyrefly_types::special_form::SpecialForm; @@ -2336,9 +2337,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { Type::TypeVar(_) => acc.push(AttributeBase1::ClassInstance( self.stdlib.type_var().clone(), )), - Type::Sentinel(_) => acc.push(AttributeBase1::ClassInstance( - self.stdlib.sentinel().clone(), - )), + Type::Sentinel(sentinel) => { + acc.push(AttributeBase1::ClassInstance(match sentinel.kind() { + SentinelKind::Builtins => self.stdlib.sentinel_builtin().clone(), + SentinelKind::TypingExtensions => { + self.stdlib.sentinel_typing_extensions().clone() + } + })) + } Type::ParamSpec(_) => acc.push(AttributeBase1::ClassInstance( self.stdlib.param_spec().clone(), )), diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 6812f7f9ee..5cab1bb0f7 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -21,6 +21,7 @@ use pyrefly_types::callable::FunctionKind; use pyrefly_types::dimension::SizeExpr; use pyrefly_types::dimension::canonicalize; use pyrefly_types::literal::LitStyle; +use pyrefly_types::sentinel::SentinelKind; use pyrefly_types::shaped_array::IndexOp; use pyrefly_types::shaped_array::ShapedArrayShape; use pyrefly_types::shaped_array::ShapedArrayType; @@ -1793,6 +1794,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { &self, assignment_name: Identifier, x: &ExprCall, + kind: SentinelKind, errors: &ErrorCollector, ) -> Sentinel { let mut sentinel_name = assignment_name; @@ -1868,7 +1870,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } - Sentinel::new(sentinel_name, self.module().dupe()) + Sentinel::new(sentinel_name, self.module().dupe(), kind) } pub fn typevar_from_call( diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index 41fef1134e..9e599ee2b6 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -5344,9 +5344,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } Binding::Delete(x) => self.check_del_statement(x, errors), Binding::Sentinel(x) => { - let (ann, name, call) = x.as_ref(); + let (ann, name, call, kind) = x.as_ref(); let ty = self - .sentinel_from_call(name.clone(), call, errors) + .sentinel_from_call(name.clone(), call, *kind, errors) .to_type(self.heap); if let Some(k) = ann && let AnnotationWithTarget { diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index 4709b2493a..f747d9e5cf 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -24,6 +24,7 @@ use pyrefly_python::symbol_kind::SymbolKind; use pyrefly_types::callable::PlaceholderBodyKind; use pyrefly_types::heap::TypeHeap; use pyrefly_types::meta_shape_dsl::ShapeDslFunction; +use pyrefly_types::sentinel::SentinelKind; use pyrefly_types::special_form::SpecialForm; use pyrefly_types::type_alias::TypeAlias; use pyrefly_types::type_alias::TypeAliasIndex; @@ -2354,7 +2355,15 @@ pub enum Binding { /// A match statement or if/elif chain that may be type-exhaustive. /// Resolves to Never if ANY narrow entry narrows to Never, None otherwise. Exhaustive(Box), - Sentinel(Box<(Option>, Identifier, Box)>), + /// Last boolean represents if the sentinel is from builtins. If false, then it is from typing_extensions. + Sentinel( + Box<( + Option>, + Identifier, + Box, + SentinelKind, + )>, + ), } impl DisplayWith for Binding { @@ -2398,8 +2407,14 @@ impl DisplayWith for Binding { write!(f, "TypeVarTuple({}, {name}, {})", ann(a), m.display(call)) } Self::Sentinel(x) => { - let (a, name, call) = x.as_ref(); - write!(f, "Sentinel({}, {name}, {})", ann(a), m.display(call)) + let (a, name, call, kind) = x.as_ref(); + write!( + f, + "{}({}, {name}, {})", + kind.name(), + ann(a), + m.display(call) + ) } Self::ReturnExplicit(x) => { write!(f, "ReturnExplicit({}, ", ann(&x.annot))?; diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 7f9a59b62a..693a1d12b3 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -10,6 +10,7 @@ use pyrefly_python::ast::Ast; use pyrefly_python::module_name::ModuleName; use pyrefly_python::nesting_context::NestingContext; use pyrefly_python::short_identifier::ShortIdentifier; +use pyrefly_types::sentinel::SentinelKind; use ruff_python_ast::Arguments; use ruff_python_ast::AtomicNodeIndex; use ruff_python_ast::Expr; @@ -281,7 +282,12 @@ impl<'a> BindingsBuilder<'a> { }) } - fn assign_sentinel(&mut self, name: &ExprName, call: &mut ExprCall) { + fn assign_sentinel( + &mut self, + name: &ExprName, + call: &mut ExprCall, + sentinel_kind: SentinelKind, + ) { // Type var declarations are static types only; skip them for first-usage type inference. let static_type_usage = &mut Usage::StaticTypeInformation { is_annotation: false, @@ -301,6 +307,7 @@ impl<'a> BindingsBuilder<'a> { ann, Ast::expr_name_identifier(name.clone()), Box::new(call.clone()), + sentinel_kind, ))) }) } @@ -623,11 +630,11 @@ impl<'a> BindingsBuilder<'a> { return; } SpecialExport::Sentinel => { - self.assign_sentinel(name, call); + self.assign_sentinel(name, call, SentinelKind::TypingExtensions); return; } SpecialExport::BuiltinsSentinel => { - self.assign_sentinel(name, call); + self.assign_sentinel(name, call, SentinelKind::Builtins); return; } SpecialExport::Enum diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index b7ea724146..e86da20ee0 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -203,3 +203,34 @@ def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None: assert_type(x, int) "#, ); + +testcase!( + test_typeshed_sentinel_3_15_no_dunder_name, + TestEnv::new().with_version(PythonVersion::new(3, 15, 0)), + r#" +from typing_extensions import Sentinel + +MISSING = Sentinel("MISSING") +MISSING.__name__ # E: Object of class `Sentinel` has no attribute `__name__` + "#, +); + +testcase!( + test_builtin_sentinel_3_15_has_dunder_name, + TestEnv::new().with_version(PythonVersion::new(3, 15, 0)), + r#" +MISSING = sentinel("MISSING") +MISSING.__name__ + "#, +); + +testcase!( + test_typeshed_sentinel_3_14_no_dunder_name, + TestEnv::new().with_version(PythonVersion::new(3, 14, 0)), + r#" +from typing_extensions import Sentinel + +MISSING = Sentinel("MISSING") +MISSING.__name__ # E: Object of class `Sentinel` has no attribute `__name__` + "#, +); From 26aad503dd5fbd22d16177a858e987226d778b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:37:16 +0000 Subject: [PATCH 07/11] Move sentinel truthy test away from test/narrow.rs --- pyrefly/lib/test/narrow.rs | 15 --------------- pyrefly/lib/test/sentinel.rs | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyrefly/lib/test/narrow.rs b/pyrefly/lib/test/narrow.rs index c28f133ad8..32bfecf527 100644 --- a/pyrefly/lib/test/narrow.rs +++ b/pyrefly/lib/test/narrow.rs @@ -3288,21 +3288,6 @@ def f(x: int | bool | MISSING): "#, ); -testcase!( - sentinel_is_truthy, - r#" -from typing_extensions import Sentinel -from typing import Literal, assert_type - -MISSING = Sentinel("MISSING") -x: MISSING | None = MISSING -if x: - assert_type(x, MISSING) -else: - assert_type(x, None) - "#, -); - testcase!( sentinel_narrow_with_type_param, r#" diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index e86da20ee0..7fd88edc47 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -234,3 +234,18 @@ MISSING = Sentinel("MISSING") MISSING.__name__ # E: Object of class `Sentinel` has no attribute `__name__` "#, ); + +testcase!( + sentinel_is_truthy, + r#" +from typing_extensions import Sentinel +from typing import Literal, assert_type + +MISSING = Sentinel("MISSING") +x: MISSING | None = MISSING +if x: + assert_type(x, MISSING) +else: + assert_type(x, None) + "#, +); From f766c281f8669289ab73757d205a6a9563063c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:51:13 +0000 Subject: [PATCH 08/11] Move more sentinel references to lowercase --- pyrefly/lib/query.rs | 7 +++---- pyrefly/lib/tsp/type_conversion.rs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyrefly/lib/query.rs b/pyrefly/lib/query.rs index bef25d15b8..03e890ce9d 100644 --- a/pyrefly/lib/query.rs +++ b/pyrefly/lib/query.rs @@ -425,10 +425,9 @@ fn type_shape_kind(context: &TypeShapeContext, ty: &Type) -> TypeShapeKind { "typing.Literal", vec![named_leaf(literal.value.to_string())], ), - Type::Sentinel(sentinel) => named_type_shape_kind( - "typing_extensions.Sentinel", - vec![named_leaf(format!("{}", sentinel))], - ), + Type::Sentinel(sentinel) => { + named_type_shape_kind("sentinel", vec![named_leaf(format!("{}", sentinel))]) + } Type::LiteralString(_) => { named_type_shape_kind("typing_extensions.LiteralString", Vec::new()) } diff --git a/pyrefly/lib/tsp/type_conversion.rs b/pyrefly/lib/tsp/type_conversion.rs index cb0891dd6d..17a1791608 100644 --- a/pyrefly/lib/tsp/type_conversion.rs +++ b/pyrefly/lib/tsp/type_conversion.rs @@ -362,7 +362,7 @@ impl TypeConverter<'_> { PyreflyType::Materialization => builtin("Unknown"), // --- Sentinel type --- - PyreflyType::Sentinel(_) => builtin("Sentinel"), + PyreflyType::Sentinel(_) => builtin("sentinel"), } } From bcce9c0cbe4523af51997c02c91d9d66411a257a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:36:02 +0000 Subject: [PATCH 09/11] Fix sentinel nesting_context usage --- crates/pyrefly_types/src/sentinel.rs | 10 +++++++--- pyrefly/lib/alt/expr.rs | 4 +++- pyrefly/lib/alt/solve.rs | 4 ++-- pyrefly/lib/binding/binding.rs | 3 ++- pyrefly/lib/binding/stmt.rs | 2 ++ pyrefly/lib/test/sentinel.rs | 13 +++++++++++++ 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/pyrefly_types/src/sentinel.rs b/crates/pyrefly_types/src/sentinel.rs index 158c53ee9d..0322c27b97 100644 --- a/crates/pyrefly_types/src/sentinel.rs +++ b/crates/pyrefly_types/src/sentinel.rs @@ -67,11 +67,15 @@ struct SentinelInner { } impl Sentinel { - pub fn new(name: Identifier, module: Module, kind: SentinelKind) -> Self { + pub fn new( + name: Identifier, + nesting_context: NestingContext, + module: Module, + kind: SentinelKind, + ) -> Self { Self(ArcId::new(SentinelInner { kind, - // TODO: properly take parent from caller of new() - qname: QName::new(name, NestingContext::toplevel(), module), + qname: QName::new(name, nesting_context, module), })) } diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 5cab1bb0f7..c413142d86 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -16,6 +16,7 @@ use itertools::Itertools; use pyrefly_python::ast::Ast; use pyrefly_python::dunder; use pyrefly_python::module_name::ModuleName; +use pyrefly_python::nesting_context::NestingContext; use pyrefly_python::short_identifier::ShortIdentifier; use pyrefly_types::callable::FunctionKind; use pyrefly_types::dimension::SizeExpr; @@ -1793,6 +1794,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { pub fn sentinel_from_call( &self, assignment_name: Identifier, + nesting_context: NestingContext, x: &ExprCall, kind: SentinelKind, errors: &ErrorCollector, @@ -1870,7 +1872,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } - Sentinel::new(sentinel_name, self.module().dupe(), kind) + Sentinel::new(sentinel_name, nesting_context, self.module().dupe(), kind) } pub fn typevar_from_call( diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index 9e599ee2b6..33e0750bf8 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -5344,9 +5344,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } Binding::Delete(x) => self.check_del_statement(x, errors), Binding::Sentinel(x) => { - let (ann, name, call, kind) = x.as_ref(); + let (ann, name, nesting_context, call, kind) = x.as_ref(); let ty = self - .sentinel_from_call(name.clone(), call, *kind, errors) + .sentinel_from_call(name.clone(), nesting_context.dupe(), call, *kind, errors) .to_type(self.heap); if let Some(k) = ann && let AnnotationWithTarget { diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index f747d9e5cf..4f77066c57 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -2360,6 +2360,7 @@ pub enum Binding { Box<( Option>, Identifier, + NestingContext, Box, SentinelKind, )>, @@ -2407,7 +2408,7 @@ impl DisplayWith for Binding { write!(f, "TypeVarTuple({}, {name}, {})", ann(a), m.display(call)) } Self::Sentinel(x) => { - let (a, name, call, kind) = x.as_ref(); + let (a, name, _, call, kind) = x.as_ref(); write!( f, "{}({}, {name}, {})", diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 693a1d12b3..9894a7f16c 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -299,6 +299,7 @@ impl<'a> BindingsBuilder<'a> { for kw in call.arguments.keywords.iter_mut() { self.ensure_expr(&mut kw.value, static_type_usage); } + let nesting_context = self.scopes.nesting_context(); // Like legacy type var, Sentinel can only be created with a single Sentinel binding to a // single variable (https://peps.python.org/pep-0661/#typing). Thus we bind it in the same // way legacy type vars are bound. @@ -306,6 +307,7 @@ impl<'a> BindingsBuilder<'a> { Binding::Sentinel(Box::new(( ann, Ast::expr_name_identifier(name.clone()), + nesting_context, Box::new(call.clone()), sentinel_kind, ))) diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index 7fd88edc47..d9b31ea54c 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -249,3 +249,16 @@ else: assert_type(x, None) "#, ); + +testcase!( + test_sentinel_in_class_body_reveal_qualified_name, + r#" +from typing import reveal_type +from typing_extensions import Sentinel + +class Cls: + IN_CLASS = Sentinel("IN_CLASS") + +reveal_type(Cls.IN_CLASS) # E: Cls.IN_CLASS + "#, +); From 8418fcadf42aa6a7b71451a6e5a110c89a0fda64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:43:47 +0000 Subject: [PATCH 10/11] Fix documentation to allow different sentinel first argument and var name --- website/docs/error-kinds.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/website/docs/error-kinds.mdx b/website/docs/error-kinds.mdx index 57c3587005..5fff46207c 100644 --- a/website/docs/error-kinds.mdx +++ b/website/docs/error-kinds.mdx @@ -892,10 +892,11 @@ An error caused by incorrect definition of a Sentinel. A few examples: ```python from typing_extensions import Sentinel -# Sentinels must be assigned to a matching variable. -A = Sentinel("B") +# First argument passed to sentinel constructor isn't a string literal +my_str: str = "MISSING" +A = Sentinel(my_str) -# Invalid arguments passed to Sentinel constructor +# Invalid arguments passed to sentinel constructor MISSING = Sentinel("MISSING", non_existent="") ``` From aad989862ca503f62b356fbf2d051afd51101f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hr=C3=B3lfur?= <33896457+hrolfurgylfa@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:46:08 +0000 Subject: [PATCH 11/11] Revert "Differentiate between typing_extensions Sentinel and builtins sentinel" This reverts commit b66942c9d21d4892a591c4d7d460fa109ea36a61. --- crates/pyrefly_types/src/sentinel.rs | 28 +--------------------------- crates/pyrefly_types/src/stdlib.rs | 19 +++++++------------ pyrefly/lib/alt/attr.rs | 12 +++--------- pyrefly/lib/alt/expr.rs | 4 +--- pyrefly/lib/alt/solve.rs | 4 ++-- pyrefly/lib/binding/binding.rs | 12 ++---------- pyrefly/lib/binding/stmt.rs | 13 +++---------- pyrefly/lib/test/sentinel.rs | 11 ----------- 8 files changed, 19 insertions(+), 84 deletions(-) diff --git a/crates/pyrefly_types/src/sentinel.rs b/crates/pyrefly_types/src/sentinel.rs index 0322c27b97..3fdd3c3055 100644 --- a/crates/pyrefly_types/src/sentinel.rs +++ b/crates/pyrefly_types/src/sentinel.rs @@ -24,21 +24,6 @@ use crate::equality::TypeEqCtx; use crate::heap::TypeHeap; use crate::types::Type; -#[derive(Clone, Copy, Dupe, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, TypeEq)] -pub enum SentinelKind { - Builtins, - TypingExtensions, -} - -impl SentinelKind { - pub fn name(&self) -> &str { - match self { - Self::Builtins => "sentinel", - Self::TypingExtensions => "Sentinel", - } - } -} - /// Used to represent Sentinel calls. Each Sentinel is unique, so use the ArcId to separate them. #[derive(Clone, Dupe, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct Sentinel(ArcId); @@ -63,26 +48,15 @@ impl Display for Sentinel { #[derive(Debug, PartialEq, TypeEq, Eq, Ord, PartialOrd)] struct SentinelInner { qname: QName, - kind: SentinelKind, } impl Sentinel { - pub fn new( - name: Identifier, - nesting_context: NestingContext, - module: Module, - kind: SentinelKind, - ) -> Self { + pub fn new(name: Identifier, nesting_context: NestingContext, module: Module) -> Self { Self(ArcId::new(SentinelInner { - kind, qname: QName::new(name, nesting_context, module), })) } - pub fn kind(&self) -> SentinelKind { - self.0.kind - } - pub fn qname(&self) -> &QName { &self.0.qname } diff --git a/crates/pyrefly_types/src/stdlib.rs b/crates/pyrefly_types/src/stdlib.rs index e73c8dd06a..1c1ae8a3b0 100644 --- a/crates/pyrefly_types/src/stdlib.rs +++ b/crates/pyrefly_types/src/stdlib.rs @@ -106,11 +106,8 @@ pub struct Stdlib { /// For 3.12 and 3.13 defined separately in both locations. type_alias_type: StdlibResult, /// Defined in `typing_extensions` as Sentinel. - /// Different attributes from `builtins` sentinel. - sentinel_typing_extensions: StdlibResult, /// Defined in `builtins` as `sentinel` since 3.15. - /// Different attributes from `typing_extensions` Sentinel. - sentinel_builtin: StdlibResult, + sentinel: StdlibResult, traceback_type: StdlibResult, builtins_type: StdlibResult, /// Introduced in Python 3.10. @@ -263,9 +260,10 @@ impl Stdlib { param_spec_kwargs: lookup_concrete(standardised(3, 10), "ParamSpecKwargs"), type_var_tuple: lookup_concrete(standardised(3, 11), "TypeVarTuple"), type_alias_type: lookup_concrete(standardised(3, 12), "TypeAliasType"), - // sentinel: lookup_concrete(typing_extensions, "Sentinel"), - sentinel_builtin: lookup_concrete(builtins, "sentinel"), - sentinel_typing_extensions: lookup_concrete(typing_extensions, "Sentinel"), + sentinel: version + .at_least(3, 15) + .then(|| lookup_concrete(builtins, "sentinel")) + .unwrap_or_else(|| lookup_concrete(typing_extensions, "Sentinel")), traceback_type: lookup_concrete(types, "TracebackType"), function_type: lookup_concrete(types, "FunctionType"), method_type: lookup_concrete(types, "MethodType"), @@ -590,11 +588,8 @@ impl Stdlib { Self::primitive(&self.type_alias_type) } - pub fn sentinel_builtin(&self) -> &ClassType { - Self::primitive(&self.sentinel_builtin) - } - pub fn sentinel_typing_extensions(&self) -> &ClassType { - Self::primitive(&self.sentinel_typing_extensions) + pub fn sentinel(&self) -> &ClassType { + Self::primitive(&self.sentinel) } pub fn traceback_type(&self) -> &ClassType { diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index d97acc524c..2c0eeec0a2 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -14,7 +14,6 @@ use pyrefly_types::dimension::SizeExpr; use pyrefly_types::heap::TypeHeap; use pyrefly_types::lit_int::LitInt; use pyrefly_types::literal::LitEnum; -use pyrefly_types::sentinel::SentinelKind; use pyrefly_types::shaped_array::ShapedArrayShape; use pyrefly_types::shaped_array::ShapedArrayType; use pyrefly_types::special_form::SpecialForm; @@ -2337,14 +2336,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { Type::TypeVar(_) => acc.push(AttributeBase1::ClassInstance( self.stdlib.type_var().clone(), )), - Type::Sentinel(sentinel) => { - acc.push(AttributeBase1::ClassInstance(match sentinel.kind() { - SentinelKind::Builtins => self.stdlib.sentinel_builtin().clone(), - SentinelKind::TypingExtensions => { - self.stdlib.sentinel_typing_extensions().clone() - } - })) - } + Type::Sentinel(_) => acc.push(AttributeBase1::ClassInstance( + self.stdlib.sentinel().clone(), + )), Type::ParamSpec(_) => acc.push(AttributeBase1::ClassInstance( self.stdlib.param_spec().clone(), )), diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index c413142d86..d31cebd79a 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -22,7 +22,6 @@ use pyrefly_types::callable::FunctionKind; use pyrefly_types::dimension::SizeExpr; use pyrefly_types::dimension::canonicalize; use pyrefly_types::literal::LitStyle; -use pyrefly_types::sentinel::SentinelKind; use pyrefly_types::shaped_array::IndexOp; use pyrefly_types::shaped_array::ShapedArrayShape; use pyrefly_types::shaped_array::ShapedArrayType; @@ -1796,7 +1795,6 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { assignment_name: Identifier, nesting_context: NestingContext, x: &ExprCall, - kind: SentinelKind, errors: &ErrorCollector, ) -> Sentinel { let mut sentinel_name = assignment_name; @@ -1872,7 +1870,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } - Sentinel::new(sentinel_name, nesting_context, self.module().dupe(), kind) + Sentinel::new(sentinel_name, nesting_context, self.module().dupe()) } pub fn typevar_from_call( diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index 33e0750bf8..f84d658f10 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -5344,9 +5344,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } Binding::Delete(x) => self.check_del_statement(x, errors), Binding::Sentinel(x) => { - let (ann, name, nesting_context, call, kind) = x.as_ref(); + let (ann, name, nesting_context, call) = x.as_ref(); let ty = self - .sentinel_from_call(name.clone(), nesting_context.dupe(), call, *kind, errors) + .sentinel_from_call(name.clone(), nesting_context.dupe(), call, errors) .to_type(self.heap); if let Some(k) = ann && let AnnotationWithTarget { diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index 4f77066c57..e3dd73bf00 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -24,7 +24,6 @@ use pyrefly_python::symbol_kind::SymbolKind; use pyrefly_types::callable::PlaceholderBodyKind; use pyrefly_types::heap::TypeHeap; use pyrefly_types::meta_shape_dsl::ShapeDslFunction; -use pyrefly_types::sentinel::SentinelKind; use pyrefly_types::special_form::SpecialForm; use pyrefly_types::type_alias::TypeAlias; use pyrefly_types::type_alias::TypeAliasIndex; @@ -2362,7 +2361,6 @@ pub enum Binding { Identifier, NestingContext, Box, - SentinelKind, )>, ), } @@ -2408,14 +2406,8 @@ impl DisplayWith for Binding { write!(f, "TypeVarTuple({}, {name}, {})", ann(a), m.display(call)) } Self::Sentinel(x) => { - let (a, name, _, call, kind) = x.as_ref(); - write!( - f, - "{}({}, {name}, {})", - kind.name(), - ann(a), - m.display(call) - ) + let (a, name, _, call) = x.as_ref(); + write!(f, "sentinel({}, {name}, {})", ann(a), m.display(call)) } Self::ReturnExplicit(x) => { write!(f, "ReturnExplicit({}, ", ann(&x.annot))?; diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 9894a7f16c..7f612a0c73 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -10,7 +10,6 @@ use pyrefly_python::ast::Ast; use pyrefly_python::module_name::ModuleName; use pyrefly_python::nesting_context::NestingContext; use pyrefly_python::short_identifier::ShortIdentifier; -use pyrefly_types::sentinel::SentinelKind; use ruff_python_ast::Arguments; use ruff_python_ast::AtomicNodeIndex; use ruff_python_ast::Expr; @@ -282,12 +281,7 @@ impl<'a> BindingsBuilder<'a> { }) } - fn assign_sentinel( - &mut self, - name: &ExprName, - call: &mut ExprCall, - sentinel_kind: SentinelKind, - ) { + fn assign_sentinel(&mut self, name: &ExprName, call: &mut ExprCall) { // Type var declarations are static types only; skip them for first-usage type inference. let static_type_usage = &mut Usage::StaticTypeInformation { is_annotation: false, @@ -309,7 +303,6 @@ impl<'a> BindingsBuilder<'a> { Ast::expr_name_identifier(name.clone()), nesting_context, Box::new(call.clone()), - sentinel_kind, ))) }) } @@ -632,11 +625,11 @@ impl<'a> BindingsBuilder<'a> { return; } SpecialExport::Sentinel => { - self.assign_sentinel(name, call, SentinelKind::TypingExtensions); + self.assign_sentinel(name, call); return; } SpecialExport::BuiltinsSentinel => { - self.assign_sentinel(name, call, SentinelKind::Builtins); + self.assign_sentinel(name, call); return; } SpecialExport::Enum diff --git a/pyrefly/lib/test/sentinel.rs b/pyrefly/lib/test/sentinel.rs index d9b31ea54c..98c800bd98 100644 --- a/pyrefly/lib/test/sentinel.rs +++ b/pyrefly/lib/test/sentinel.rs @@ -204,17 +204,6 @@ def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None: "#, ); -testcase!( - test_typeshed_sentinel_3_15_no_dunder_name, - TestEnv::new().with_version(PythonVersion::new(3, 15, 0)), - r#" -from typing_extensions import Sentinel - -MISSING = Sentinel("MISSING") -MISSING.__name__ # E: Object of class `Sentinel` has no attribute `__name__` - "#, -); - testcase!( test_builtin_sentinel_3_15_has_dunder_name, TestEnv::new().with_version(PythonVersion::new(3, 15, 0)),