Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/pyrefly_config/src/error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
14 changes: 14 additions & 0 deletions crates/pyrefly_types/src/equality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -47,6 +48,7 @@ pub struct TypeEqCtx {
param_spec: SmallMap<ParamSpec, ParamSpec>,
type_var: SmallMap<TypeVar, TypeVar>,
type_var_tuple: SmallMap<TypeVarTuple, TypeVarTuple>,
sentinel: SmallMap<Sentinel, Sentinel>,
/// Alpha-equivalence mapping for Quantified binders: LHS identity → RHS identity.
/// First pairing wins; subsequent occurrences must match.
quantified_identity: SmallMap<QuantifiedIdentity, QuantifiedIdentity>,
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions crates/pyrefly_types/src/heap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Type::Union(Box::new(Union {
Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
93 changes: 93 additions & 0 deletions crates/pyrefly_types/src/sentinel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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;

#[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<SentinelInner>);

// This is a lie, we do have types in the bound position
impl Visit<Type> for Sentinel {
const RECURSE_CONTAINS: bool = false;
fn recurse<'a>(&'a self, _: &mut dyn FnMut(&'a Type)) {}
}

impl VisitMut<Type> 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,
kind: SentinelKind,
}

impl Sentinel {
pub fn new(name: Identifier, module: Module, kind: SentinelKind) -> Self {
Self(ArcId::new(SentinelInner {
kind,
// TODO: properly take parent from caller of new()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can grab the nesting context from the Scopes stack when constructing Binding::Sentinel - see comment in stmt.rs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed

qname: QName::new(name, NestingContext::toplevel(), module),
}))
}

pub fn kind(&self) -> SentinelKind {
self.0.kind
}

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)
}
}
16 changes: 16 additions & 0 deletions crates/pyrefly_types/src/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ 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<ClassType>,
/// Defined in `typing_extensions` as Sentinel.
/// Different attributes from `builtins` sentinel.
sentinel_typing_extensions: StdlibResult<ClassType>,
/// Defined in `builtins` as `sentinel` since 3.15.
/// Different attributes from `typing_extensions` Sentinel.
sentinel_builtin: StdlibResult<ClassType>,
traceback_type: StdlibResult<ClassType>,
builtins_type: StdlibResult<ClassType>,
/// Introduced in Python 3.10.
Expand Down Expand Up @@ -257,6 +263,9 @@ 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"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How different are builtins.sentinel and typing_extensions.Sentinel? If they're pretty similar, I'd suggest combining them into just one sentinel, which is looked up from builtins in 3.15+ and from typing_extensions otherwise, and modifying the rest of the PR to likewise stop tracking the distinction between the two.

In general, we treat the typing and typing_extensions versions of things as the same without worrying about the minor differences between their runtime definitions. And distinguishing between builtins.sentinel and typing_extensions.Sentinel seems to be adding a fair bit of complexity to this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The main reason I differentiated between the two was the __name__ attribute that is only available on the builtins sentinel, not the typing extensions Sentinel. If we merge them it would mean that in Python 3.15+ pyrefly would say that accessing __name__ on typing_extensions Sentinel is allowed and returns a string, while raising a runtime error if run. If that is fine I can go ahead and remove this logic.

Here is how I confirmed this difference if you want to test it with the python 3.15 beta:

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 "<string>", line 1, in <module>
>     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

Also, if this ends up being kept, I should probably add a comment explaining the reasoning better, and remove that commented out line, not sure how that one got past me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation! If the presence/absence of the __name__ attribute is the main difference, then I'm in favor of merging them.

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"),
Expand Down Expand Up @@ -581,6 +590,13 @@ 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 traceback_type(&self) -> &ClassType {
Self::primitive(&self.traceback_type)
}
Expand Down
8 changes: 8 additions & 0 deletions crates/pyrefly_types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TypeAliasData>),
// 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:
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
}
}
Expand All @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions pyrefly/lib/alt/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2336,6 +2337,14 @@ 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::ParamSpec(_) => acc.push(AttributeBase1::ClassInstance(
self.stdlib.param_spec().clone(),
)),
Expand Down
85 changes: 85 additions & 0 deletions pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -95,6 +96,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;
Expand Down Expand Up @@ -1788,6 +1790,89 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}
}

pub fn sentinel_from_call(
&self,
assignment_name: Identifier,
x: &ExprCall,
kind: SentinelKind,
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 {
sentinel_name = Identifier::new(lit.value.to_str(), lit.range());
} 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(sentinel_name, self.module().dupe(), kind)
}

pub fn typevar_from_call(
&self,
name: Identifier,
Expand Down
Loading
Loading