Implement the sentinel builtin (PEP 661) (#8945)

* Implement the sentinel builtin from PEP 661

Add unique sentinel values with copy/pickle identity, caller module capture, union support, and cycle collection. Compare constructor behavior and six upstream tests with CPython v3.15.0rc2; retain the existing Python target and bundled libraries.

Assisted-by: Codex:model-version-unavailable

* Remove duplicate sentinel snippet tests

Rely on the canonical Lib/test/test_builtin.py sentinel cases when the standard library is updated, as requested in PR #8945 review. Remove the custom snippet without changing the runtime implementation or existing standard-library tests.

Assisted-by: Codex:model-version-unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-05 16:20:37 +09:00
1 parent f46ba48f10
commit f1ebf16ce0
6 files changed
+128 -2

No files matched your search

+2
View File
@@ -75,6 +75,8 @@ pub(crate) mod type_;
pub use type_::{PyType, PyTypeRef};
pub(crate) mod range;
pub use range::PyRange;
pub(crate) mod sentinel;
pub use sentinel::PySentinel;
pub(crate) mod set;
pub use set::{PyFrozenSet, PySet};
pub(crate) mod singletons;
+118
View File
@@ -0,0 +1,118 @@
use super::{PyStrRef, PyType, union_};
use crate::{
Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
class::PyClassImpl,
object::{PyAtomicRef, Traverse, TraverseFn},
protocol::PyNumberMethods,
types::{AsNumber, Constructor, Representable},
};
/// Create a unique sentinel object with the given name.
#[pyclass(module = false, name = "sentinel", traverse = "manual")]
#[derive(Debug)]
pub struct PySentinel {
#[pymember(name = "__name__", type = "object_ex")]
name: PyStrRef,
#[pymember(name = "__module__", type = "object_ex", writable)]
module: PyAtomicRef<Option<PyObject>>,
repr: Option<PyStrRef>,
}
// SAFETY: Visit each owned Python reference once, without cloning it.
unsafe impl Traverse for PySentinel {
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
self.name.traverse(tracer_fn);
self.module.traverse(tracer_fn);
self.repr.traverse(tracer_fn);
}
fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
// String subclasses may own references back to this sentinel.
let name = core::mem::replace(&mut self.name, Context::genesis().empty_str.to_owned());
out.push(name.into());
if let Some(module) = self.module.store(None) {
out.push(module);
}
if let Some(repr) = self.repr.take() {
out.push(repr.into());
}
}
}
impl PyPayload for PySentinel {
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.sentinel_type
}
}
#[derive(FromArgs)]
pub struct SentinelArgs {
#[pyarg(positional, error_msg = "sentinel() argument 1 must be str")]
name: PyStrRef,
#[pyarg(
named,
optional,
error_msg = "sentinel() argument 'repr' must be str or None"
)]
repr: Option<PyStrRef>,
}
impl Constructor for PySentinel {
type Args = SentinelArgs;
fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
let module = if let Some(frame) = vm.current_frame()
&& let Some(function) = frame.iframe().func_obj()
{
// Use the module captured by the caller function, including the
// synthetic functions used for module, eval, and exec code.
function.get_attr(identifier!(vm, __module__), vm)?
} else {
vm.ctx.none()
};
Ok(Self {
name: args.name,
module: PyAtomicRef::from(Some(module)),
repr: args.repr,
})
}
}
#[pyclass(with(Constructor, Representable, AsNumber), flags(HAVE_GC))]
impl PySentinel {
#[pymethod]
fn __copy__(zelf: PyRef<Self>) -> PyRef<Self> {
zelf
}
#[pymethod]
fn __deepcopy__(zelf: PyRef<Self>, _memo: PyObjectRef) -> PyRef<Self> {
zelf
}
#[pymethod]
fn __reduce__(zelf: &Py<Self>) -> PyStrRef {
zelf.name.clone()
}
}
impl Representable for PySentinel {
fn repr(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyStrRef> {
Ok(zelf.repr.as_ref().unwrap_or(&zelf.name).clone())
}
}
impl AsNumber for PySentinel {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| union_::or_op(a.to_owned(), b.to_owned(), vm)),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER
}
}
pub(crate) fn init(context: &'static Context) {
PySentinel::extend_class(context, context.types.sentinel_type);
}
+1
View File
@@ -202,6 +202,7 @@ impl Py<PyUnion> {
fn is_unionable(obj: &PyObject, vm: &VirtualMachine) -> bool {
let cls = obj.class();
cls.is(vm.ctx.types.none_type)
|| cls.is(vm.ctx.types.sentinel_type)
|| obj.downcastable::<PyType>()
|| cls.fast_issubclass(vm.ctx.types.generic_alias_type)
|| cls.is(vm.ctx.types.union_type)
+1
View File
@@ -1479,6 +1479,7 @@ pub fn init_module(vm: &VirtualMachine, module: &Py<PyModule>) {
"object" => ctx.types.object_type.to_owned(),
"property" => ctx.types.property_type.to_owned(),
"range" => ctx.types.range_type.to_owned(),
"sentinel" => ctx.types.sentinel_type.to_owned(),
"set" => ctx.types.set_type.to_owned(),
"slice" => ctx.types.slice_type.to_owned(),
"staticmethod" => ctx.types.staticmethod_type.to_owned(),
+1
View File
@@ -253,6 +253,7 @@ bitflagset::bitflagset! {
const IMMUTABLETYPE = 8;
const HEAPTYPE = 9;
const BASETYPE = 10;
const HAVE_GC = 14;
const METHOD_DESCRIPTOR = 17;
const IS_ABSTRACT = 20;
// Built-in types that match the subject itself in pattern matching
+5 -2
View File
@@ -4,8 +4,8 @@ use crate::{
asyncgenerator, bool_, builtin_func, bytearray, bytes, capsule, classmethod, code, complex,
coroutine, descriptor, dict, enumerate, filter, float, frame, frame_locals_proxy, function,
generator, genericalias, getset, int, interpolation, iter, list, map, mappingproxy, memory,
module, namespace, object, property, pystr, range, set, singletons, slice, staticmethod,
super_, template, traceback, tuple,
module, namespace, object, property, pystr, range, sentinel, set, singletons, slice,
staticmethod, super_, template, traceback, tuple,
type_::{self, PyType},
union_, weakproxy, weakref, zip,
},
@@ -64,6 +64,7 @@ pub struct TypeZoo {
pub memoryviewiterator_type: &'static Py<PyType>,
pub tuple_type: &'static Py<PyType>,
pub tuple_iterator_type: &'static Py<PyType>,
pub sentinel_type: &'static Py<PyType>,
pub set_type: &'static Py<PyType>,
pub set_iterator_type: &'static Py<PyType>,
pub staticmethod_type: &'static Py<PyType>,
@@ -145,6 +146,7 @@ impl TypeZoo {
memoryview_type: memory::PyMemoryView::init_builtin_type(),
property_type: property::PyProperty::init_builtin_type(),
range_type: range::PyRange::init_builtin_type(),
sentinel_type: sentinel::PySentinel::init_builtin_type(),
set_type: set::PySet::init_builtin_type(),
slice_type: slice::PySlice::init_builtin_type(),
staticmethod_type: staticmethod::PyStaticMethod::init_builtin_type(),
@@ -265,6 +267,7 @@ impl TypeZoo {
weakref::init(context);
weakproxy::init(context);
singletons::init(context);
sentinel::init(context);
module::init(context);
namespace::init(context);
mappingproxy::init(context);