Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
71 changes: 71 additions & 0 deletions crates/pyrefly_types/src/sentinel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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<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,
}

impl Sentinel {
pub fn new(name: Identifier, nesting_context: NestingContext, module: Module) -> Self {
Self(ArcId::new(SentinelInner {
qname: QName::new(name, nesting_context, 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)
}
}
11 changes: 11 additions & 0 deletions crates/pyrefly_types/src/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ 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.
/// Defined in `builtins` as `sentinel` since 3.15.
sentinel: StdlibResult<ClassType>,
traceback_type: StdlibResult<ClassType>,
builtins_type: StdlibResult<ClassType>,
/// Introduced in Python 3.10.
Expand Down Expand Up @@ -257,6 +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: 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"),
Expand Down Expand Up @@ -581,6 +588,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)
}
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
3 changes: 3 additions & 0 deletions pyrefly/lib/alt/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)),
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 @@ -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;
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,
nesting_context: NestingContext,
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 {
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, nesting_context, self.module().dupe())
}

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