Skip to content

Commit 4778b67

Browse files
hrolfurgylfaHrólfur
authored andcommitted
Add Sentinel support (PEP-661)
1 parent 53453fb commit 4778b67

22 files changed

Lines changed: 511 additions & 5 deletions

File tree

crates/pyrefly_config/src/error_kind.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ pub enum ErrorKind {
220220
/// A use of `typing.Self` in a context where Pyrefly does not recognize it as
221221
/// mapping to a valid class type.
222222
InvalidSelfType,
223+
/// An error caused by incorrect usage or definition of a Sentinel.
224+
InvalidSentinel,
223225
/// Attempting to call `super()` in a way that is not allowed.
224226
/// e.g. calling `super(Y, x)` on an object `x` that does not match the class `Y`.
225227
InvalidSuperCall,

crates/pyrefly_types/src/display.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ impl<'a> TypeDisplayContext<'a> {
570570
output.write_qname(t.qname())?;
571571
output.write_str("]")
572572
}
573+
Type::Sentinel(t) => output.write_qname(t.qname()),
573574
Type::TypeVarTuple(t) => {
574575
let type_var_tuple_qname = self.stdlib.map(|s| s.type_var_tuple().qname());
575576
output.write_builtin("TypeVarTuple", type_var_tuple_qname)?;

crates/pyrefly_types/src/equality.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use vec1::Vec1;
2828

2929
use crate::param_spec::ParamSpec;
3030
use crate::quantified::QuantifiedIdentity;
31+
use crate::sentinel::Sentinel;
3132
use crate::type_var::TypeVar;
3233
use crate::type_var_tuple::TypeVarTuple;
3334

@@ -47,6 +48,7 @@ pub struct TypeEqCtx {
4748
param_spec: SmallMap<ParamSpec, ParamSpec>,
4849
type_var: SmallMap<TypeVar, TypeVar>,
4950
type_var_tuple: SmallMap<TypeVarTuple, TypeVarTuple>,
51+
sentinel: SmallMap<Sentinel, Sentinel>,
5052
/// Alpha-equivalence mapping for Quantified binders: LHS identity → RHS identity.
5153
/// First pairing wins; subsequent occurrences must match.
5254
quantified_identity: SmallMap<QuantifiedIdentity, QuantifiedIdentity>,
@@ -144,6 +146,18 @@ impl TypeEq for TypeVarTuple {
144146
}
145147
}
146148

149+
impl TypeEq for Sentinel {
150+
fn type_eq(&self, other: &Self, ctx: &mut TypeEqCtx) -> bool {
151+
type_eq_identity(
152+
self,
153+
other,
154+
ctx,
155+
|ctx| &mut ctx.sentinel,
156+
|ctx| self.type_eq_inner(other, ctx),
157+
)
158+
}
159+
}
160+
147161
pub trait TypeEq: Eq {
148162
fn type_eq(&self, other: &Self, ctx: &mut TypeEqCtx) -> bool {
149163
let _ = ctx;

crates/pyrefly_types/src/heap.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use crate::literal::Literal;
3737
use crate::module::ModuleType;
3838
use crate::param_spec::ParamSpec;
3939
use crate::quantified::Quantified;
40+
use crate::sentinel::Sentinel;
4041
use crate::special_form::SpecialForm;
4142
use crate::tensor::TensorType;
4243
use crate::tuple::Tuple;
@@ -137,6 +138,11 @@ impl TypeHeap {
137138
Type::None
138139
}
139140

141+
/// Create a `Type::Sentinel`.
142+
pub fn mk_sentinel(&self, sentinel: Sentinel) -> Type {
143+
Type::Sentinel(sentinel)
144+
}
145+
140146
/// Create a `Type::Union` from members.
141147
pub fn mk_union(&self, members: Vec<Type>) -> Type {
142148
Type::Union(Box::new(Union {

crates/pyrefly_types/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub mod module;
3838
pub mod param_spec;
3939
pub mod quantified;
4040
pub mod read_only;
41+
pub mod sentinel;
4142
pub mod simplify;
4243
pub mod special_form;
4344
pub mod stdlib;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use std::fmt;
9+
use std::fmt::Display;
10+
use std::hash::Hash;
11+
12+
use dupe::Dupe;
13+
use pyrefly_derive::TypeEq;
14+
use pyrefly_python::module::Module;
15+
use pyrefly_python::nesting_context::NestingContext;
16+
use pyrefly_python::qname::QName;
17+
use pyrefly_util::arc_id::ArcId;
18+
use pyrefly_util::visit::Visit;
19+
use pyrefly_util::visit::VisitMut;
20+
use ruff_python_ast::Identifier;
21+
22+
use crate::equality::TypeEq;
23+
use crate::equality::TypeEqCtx;
24+
use crate::heap::TypeHeap;
25+
use crate::types::Type;
26+
27+
/// Used to represent Sentinel calls. Each Sentinel is unique, so use the ArcId to separate them.
28+
#[derive(Clone, Dupe, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
29+
pub struct Sentinel(ArcId<SentinelInner>);
30+
31+
// This is a lie, we do have types in the bound position
32+
impl Visit<Type> for Sentinel {
33+
const RECURSE_CONTAINS: bool = false;
34+
fn recurse<'a>(&'a self, _: &mut dyn FnMut(&'a Type)) {}
35+
}
36+
37+
impl VisitMut<Type> for Sentinel {
38+
const RECURSE_CONTAINS: bool = false;
39+
fn recurse_mut(&mut self, _: &mut dyn FnMut(&mut Type)) {}
40+
}
41+
42+
impl Display for Sentinel {
43+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44+
write!(f, "{}", self.0.qname.id())
45+
}
46+
}
47+
48+
#[derive(Debug, PartialEq, TypeEq, Eq, Ord, PartialOrd)]
49+
struct SentinelInner {
50+
qname: QName,
51+
}
52+
53+
impl Sentinel {
54+
pub fn new(name: Identifier, module: Module) -> Self {
55+
Self(ArcId::new(SentinelInner {
56+
// TODO: properly take parent from caller of new()
57+
qname: QName::new(name, NestingContext::toplevel(), module),
58+
}))
59+
}
60+
61+
pub fn qname(&self) -> &QName {
62+
&self.0.qname
63+
}
64+
65+
pub fn to_type(&self, heap: &TypeHeap) -> Type {
66+
heap.mk_sentinel(self.dupe())
67+
}
68+
69+
pub fn type_eq_inner(&self, other: &Self, ctx: &mut TypeEqCtx) -> bool {
70+
self.0.type_eq(&other.0, ctx)
71+
}
72+
}

crates/pyrefly_types/src/stdlib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ pub struct Stdlib {
105105
/// After 3.14, `typing_extensions` reexports from `typing`.
106106
/// For 3.12 and 3.13 defined separately in both locations.
107107
type_alias_type: StdlibResult<ClassType>,
108+
/// Defined in `typing_extensions` until 3.15, defined in `typing` since 3.15.
109+
sentinel: StdlibResult<ClassType>,
108110
traceback_type: StdlibResult<ClassType>,
109111
builtins_type: StdlibResult<ClassType>,
110112
/// Introduced in Python 3.10.
@@ -257,6 +259,7 @@ impl Stdlib {
257259
param_spec_kwargs: lookup_concrete(standardised(3, 10), "ParamSpecKwargs"),
258260
type_var_tuple: lookup_concrete(standardised(3, 11), "TypeVarTuple"),
259261
type_alias_type: lookup_concrete(standardised(3, 12), "TypeAliasType"),
262+
sentinel: lookup_concrete(standardised(3, 15), "Sentinel"),
260263
traceback_type: lookup_concrete(types, "TracebackType"),
261264
function_type: lookup_concrete(types, "FunctionType"),
262265
method_type: lookup_concrete(types, "MethodType"),
@@ -577,6 +580,10 @@ impl Stdlib {
577580
Self::primitive(&self.type_alias_type)
578581
}
579582

583+
pub fn sentinel(&self) -> &ClassType {
584+
Self::primitive(&self.sentinel)
585+
}
586+
580587
pub fn traceback_type(&self) -> &ClassType {
581588
Self::primitive(&self.traceback_type)
582589
}

crates/pyrefly_types/src/types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ use crate::literal::Literal;
5858
use crate::module::ModuleType;
5959
use crate::param_spec::ParamSpec;
6060
use crate::quantified::Quantified;
61+
use crate::sentinel::Sentinel;
6162
use crate::simplify::unions;
6263
use crate::special_form::SpecialForm;
6364
use crate::stdlib::Stdlib;
@@ -857,6 +858,9 @@ pub enum Type {
857858
/// be immediately looked up for untyping (see `TypeAliasData::TypeAliasRef`), `UntypedAlias`
858859
/// stores a reference that is untyped once we actually look up the value.
859860
UntypedAlias(Box<TypeAliasData>),
861+
// Sentinel types, documented here: https://docs.python.org/3.15/library/functions.html#sentinel
862+
// First introduced in PEP 661: https://peps.python.org/pep-0661/
863+
Sentinel(Sentinel),
860864
/// Represents the result of a super() call. The first ClassType is the point in the MRO that attribute lookup
861865
/// on the super instance should start at (*not* the class passed to the super() call), and the second
862866
/// ClassType is the second argument (implicit or explicit) to the super() call. For example, in:
@@ -918,6 +922,7 @@ impl Visit for Type {
918922
Type::Annotated(x, _metadata) => x.visit(f),
919923
Type::Unpack(x) => x.visit(f),
920924
Type::TypeVar(x) => x.visit(f),
925+
Type::Sentinel(x) => x.visit(f),
921926
Type::ParamSpec(x) => x.visit(f),
922927
Type::TypeVarTuple(x) => x.visit(f),
923928
Type::SpecialForm(x) => x.visit(f),
@@ -974,6 +979,7 @@ impl VisitMut for Type {
974979
Type::Annotated(x, _metadata) => x.visit_mut(f),
975980
Type::Unpack(x) => x.visit_mut(f),
976981
Type::TypeVar(x) => x.visit_mut(f),
982+
Type::Sentinel(x) => x.visit_mut(f),
977983
Type::ParamSpec(x) => x.visit_mut(f),
978984
Type::TypeVarTuple(x) => x.visit_mut(f),
979985
Type::SpecialForm(x) => x.visit_mut(f),
@@ -1924,6 +1930,7 @@ impl Type {
19241930
Type::ParamSpec(t) => Some(t.qname()),
19251931
Type::SelfType(cls) => Some(cls.qname()),
19261932
Type::Literal(lit) if let Lit::Enum(e) = &lit.value => Some(e.class.qname()),
1933+
Type::Sentinel(s) => Some(s.qname()),
19271934
_ => None,
19281935
}
19291936
}
@@ -1937,6 +1944,7 @@ impl Type {
19371944
Type::Literal(lit) if let Lit::Str(x) = &lit.value => Some(!x.is_empty()),
19381945
Type::Type(_) => Some(true),
19391946
Type::None => Some(false),
1947+
Type::Sentinel(_) => Some(true),
19401948
Type::Tuple(Tuple::Concrete(elements)) => Some(!elements.is_empty()),
19411949
Type::Union(u) => {
19421950
let mut answer = None;

pyrefly/lib/alt/attr.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,6 +2336,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
23362336
Type::TypeVar(_) => acc.push(AttributeBase1::ClassInstance(
23372337
self.stdlib.type_var().clone(),
23382338
)),
2339+
Type::Sentinel(_) => acc.push(AttributeBase1::ClassInstance(
2340+
self.stdlib.sentinel().clone(),
2341+
)),
23392342
Type::ParamSpec(_) => acc.push(AttributeBase1::ClassInstance(
23402343
self.stdlib.param_spec().clone(),
23412344
)),

pyrefly/lib/alt/expr.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ use crate::types::literal::Lit;
9494
use crate::types::param_spec::ParamSpec;
9595
use crate::types::quantified::Quantified;
9696
use crate::types::quantified::QuantifiedKind;
97+
use crate::types::sentinel::Sentinel;
9798
use crate::types::special_form::SpecialForm;
9899
use crate::types::tuple::Tuple;
99100
use crate::types::type_info::TypeInfo;
@@ -1799,6 +1800,97 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
17991800
}
18001801
}
18011802

1803+
pub fn sentinel_from_call(
1804+
&self,
1805+
name: Identifier,
1806+
x: &ExprCall,
1807+
errors: &ErrorCollector,
1808+
) -> Sentinel {
1809+
let mut iargs = x.arguments.args.iter();
1810+
if let Some(arg) = iargs.next() {
1811+
if let Expr::StringLiteral(lit) = arg {
1812+
if lit.value.to_str() != name.id.as_str() {
1813+
self.error(
1814+
errors,
1815+
x.range,
1816+
ErrorKind::InvalidSentinel,
1817+
format!(
1818+
"Sentinel must be assigned to a variable named `{}`",
1819+
lit.value.to_str()
1820+
),
1821+
);
1822+
}
1823+
} else {
1824+
self.error(
1825+
errors,
1826+
arg.range(),
1827+
ErrorKind::InvalidSentinel,
1828+
"Expected first argument of Sentinel to be a string literal".to_owned(),
1829+
);
1830+
}
1831+
} else {
1832+
self.error(
1833+
errors,
1834+
x.range,
1835+
ErrorKind::InvalidSentinel,
1836+
"Sentinel requires a name as the first argument".to_owned(),
1837+
);
1838+
}
1839+
if let Some(arg) = iargs.next() {
1840+
let args_range_end = x.arguments.args.last().map(|arg| arg.range().end());
1841+
let range = TextRange::new(
1842+
arg.range().start(),
1843+
// args_range_end should never be None as it should only be None if there are
1844+
// no args, but no reason not to have a default here anyway.
1845+
args_range_end.unwrap_or_else(|| arg.range().end()),
1846+
);
1847+
self.error(
1848+
errors,
1849+
range,
1850+
ErrorKind::InvalidSentinel,
1851+
"Sentinel only takes one positional argument".to_owned(),
1852+
);
1853+
}
1854+
1855+
for kw in &x.arguments.keywords {
1856+
match &kw.arg {
1857+
Some(id) => match id.id.as_str() {
1858+
"repr" => {
1859+
let got = self.expr_infer(&kw.value, errors);
1860+
if !self
1861+
.is_subset_eq(&got, &self.heap.mk_class_type(self.stdlib.str().clone()))
1862+
{
1863+
self.error(
1864+
errors,
1865+
kw.range,
1866+
ErrorKind::InvalidSentinel,
1867+
format!("Invalid type for Sentinel `repr` {got}"),
1868+
);
1869+
}
1870+
}
1871+
_ => {
1872+
self.error(
1873+
errors,
1874+
kw.range,
1875+
ErrorKind::InvalidSentinel,
1876+
format!("Unexpected keyword argument `{}` to Sentinel", id.id),
1877+
);
1878+
}
1879+
},
1880+
_ => {
1881+
self.error(
1882+
errors,
1883+
kw.range,
1884+
ErrorKind::InvalidSentinel,
1885+
"Cannot pass unpacked keyword arguments to Sentinel".to_owned(),
1886+
);
1887+
}
1888+
}
1889+
}
1890+
1891+
Sentinel::new(name, self.module().dupe())
1892+
}
1893+
18021894
pub fn typevar_from_call(
18031895
&self,
18041896
name: Identifier,

0 commit comments

Comments
 (0)