Give builtin_method its own type (#9004)

Split `builtin_method` out as a subclass of `builtin_function_or_method`.
Ordinary descriptor bind and `__new__` wrappers allocate a CFunction;
only `build_bound_method` uses the METH_METHOD payload. Keep
`types.BuiltinMethodType` as an alias of `BuiltinFunctionType`.

Assisted-by: Grok 4.6
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-10-08 14:01:52 +09:00
1 parent 1e72b22470
commit ca9dbb40f8
6 files changed
+80 -31

No files matched your search

+61 -9
View File
@@ -233,16 +233,20 @@ impl PyNativeFunction {
}
}
// PyCMethodObject in CPython
#[pyclass(name = "builtin_function_or_method", module = false, base = PyNativeFunction, ctx = "builtin_function_or_method_type")]
// Bound METH_METHOD object. The payload starts with PyNativeFunction so it can
// be read as a builtin function. The Python type is `builtin_method`.
#[repr(C)]
#[pyclass(
name = "builtin_method",
module = false,
base = PyNativeFunction,
ctx = "builtin_method_type"
)]
pub struct PyNativeMethod {
pub(crate) func: PyNativeFunction,
pub(crate) class: &'static Py<PyType>, // TODO: the actual life is &'self
pub(crate) class: &'static Py<PyType>,
}
// All Python-visible behavior (getters, slots) is registered by PyNativeFunction::extend_class.
// PyNativeMethod only extends the Rust-side struct with the defining class reference.
// The func field at offset 0 (#[repr(C)]) allows NativeFunctionOrMethod to read it safely.
#[pyclass(flags(HAS_WEAKREF, DISALLOW_INSTANTIATION))]
impl PyNativeMethod {}
@@ -296,6 +300,7 @@ pub(crate) fn init(context: &'static Context) {
.slots
.vectorcall
.store(Some(vectorcall_native_function));
PyNativeMethod::extend_class(context, context.types.builtin_method_type);
}
/// Wrapper that provides access to the common PyNativeFunction data
@@ -306,12 +311,59 @@ impl TryFromObject for NativeFunctionOrMethod {
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
let class = vm.ctx.types.builtin_function_or_method_type;
if obj.fast_isinstance(class) {
// Both PyNativeFunction and PyNativeMethod share the same type now.
// PyNativeMethod has `func: PyNativeFunction` as its first field,
// so we can safely treat the data pointer as PyNativeFunction for reading.
// `builtin_method` is a subclass; the payload starts with PyNativeFunction.
Ok(Self(unsafe { obj.downcast_unchecked() }))
} else {
Err(vm.new_downcast_type_error(class, &obj))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
Interpreter, PyObjectRef,
function::{PyMethodDef, PyMethodFlags},
};
#[test]
fn builtin_method_is_subclass_of_builtin_function() {
Interpreter::without_stdlib(Default::default()).enter(|vm| {
let function_type = vm.ctx.types.builtin_function_or_method_type;
let method_type = vm.ctx.types.builtin_method_type;
assert!(!function_type.is(method_type));
assert_eq!(&*function_type.name(), "builtin_function_or_method");
assert_eq!(&*method_type.name(), "builtin_method");
assert!(method_type.fast_issubclass(function_type));
});
}
#[test]
fn bound_instance_method_uses_function_type() {
fn identity(value: PyObjectRef) -> PyObjectRef {
value
}
const DEF: PyMethodDef = PyMethodDef::new_const(
"identity",
identity,
PyMethodFlags::METHOD,
crate::function::ItemDoc::NONE,
);
Interpreter::without_stdlib(Default::default()).enter(|vm| {
let bound = DEF.build_bound_function(&vm.ctx, vm.ctx.none());
assert!(
bound
.class()
.is(vm.ctx.types.builtin_function_or_method_type)
);
assert!(!bound.as_object().downcastable::<PyNativeMethod>());
let method = DEF.build_bound_method(&vm.ctx, vm.ctx.none(), vm.ctx.types.object_type);
assert!(method.class().is(vm.ctx.types.builtin_method_type));
assert!(method.as_object().downcastable::<PyNativeFunction>());
assert!(method.as_object().downcastable::<PyNativeMethod>());
});
}
}
+5 -5
View File
@@ -1,7 +1,7 @@
use super::{PyStr, PyStrInterned, PyTuple, PyType};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyTypeRef, builtin_func::PyNativeMethod, type_},
builtins::{PyTypeRef, builtin_func::PyNativeFunction, type_},
class::PyClassImpl,
common::hash::PyHash,
convert::{ToPyObject, ToPyResult},
@@ -124,8 +124,8 @@ impl Callable for PyMethodDescriptor {
}
impl PyMethodDescriptor {
pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef<PyNativeMethod> {
self.method.build_bound_method(ctx, obj, self.common.typ)
pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef<PyNativeFunction> {
self.method.build_bound_function(ctx, obj)
}
}
@@ -195,8 +195,8 @@ impl PyClassMethodDescriptor {
}
}
pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef<PyNativeMethod> {
self.method.build_bound_method(ctx, obj, self.common.typ)
pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef<PyNativeFunction> {
self.method.build_bound_function(ctx, obj)
}
}
+3 -3
View File
@@ -375,9 +375,9 @@ pub trait PyClassImpl: PyClassDef {
&& object_new.is_some_and(|obj_new| fn_addr(slot_new) == fn_addr(obj_new));
if !is_inherited_from_object {
let bound_new =
ctx.slot_new_wrapper
.build_bound_method(ctx, class.to_owned().into(), class);
let bound_new = ctx
.slot_new_wrapper
.build_bound_function(ctx, class.to_owned().into());
class.set_attr(identifier!(ctx, __new__), bound_new.into());
}
}
+4 -12
View File
@@ -249,7 +249,7 @@ impl PyMethodDef {
) -> PyRef<PyNativeMethod> {
PyRef::new_ref(
self.to_bound_method(obj, class),
ctx.types.builtin_function_or_method_type.to_owned(),
ctx.types.builtin_method_type.to_owned(),
None,
)
}
@@ -267,18 +267,10 @@ impl PyMethodDef {
&'static self,
ctx: &Context,
class: &'static Py<PyType>,
) -> PyRef<PyNativeMethod> {
) -> PyRef<PyNativeFunction> {
debug_assert!(self.flags.contains(PyMethodFlags::STATIC));
// Set zelf to the class (m_self = type for static methods).
// Callable::call skips prepending when STATIC flag is set.
let func = PyNativeFunction {
zelf: Some(class.to_owned().into()),
value: self,
module_object: None,
module: crate::object::PyAtomicRef::new_empty(),
_method_def_owner: None,
};
PyNativeMethod { func, class }.into_ref(ctx)
// m_self is the type; STATIC skips prepending it on call.
self.build_bound_function(ctx, class.to_owned().into())
}
/// Concatenate method groups. A pending body is copied from `docs`, then cleared.
+2 -2
View File
@@ -121,7 +121,7 @@ impl TypeZoo {
let weakref_type = weakref::PyWeak::init_manually(hierarchy.weakref_type);
let int_type = int::PyInt::init_builtin_type();
// builtin_function_or_method and builtin_method share the same type (CPython behavior)
// `builtin_method` is a subclass and is initialized once this base exists.
let builtin_function_or_method_type = builtin_func::PyNativeFunction::init_builtin_type();
let types = Self {
@@ -164,7 +164,7 @@ impl TypeZoo {
anext_awaitable: asyncgenerator::PyAnextAwaitable::init_builtin_type(),
bound_method_type: function::PyBoundMethod::init_builtin_type(),
builtin_function_or_method_type,
builtin_method_type: builtin_function_or_method_type,
builtin_method_type: builtin_func::PyNativeMethod::init_builtin_type(),
bytearray_iterator_type: bytearray::PyByteArrayIterator::init_builtin_type(),
bytes_iterator_type: bytes::PyBytesIterator::init_builtin_type(),
callable_iterator: iter::PyCallableIterator::init_builtin_type(),
+5
View File
@@ -654,7 +654,12 @@ assert repr(my_repr_func).startswith("<function my_repr_func at 0x")
# https://github.com/RustPython/RustPython/issues/3100
assert types.BuiltinMethodType is types.BuiltinFunctionType
assert issubclass(types.BuiltinMethodType, types.BuiltinFunctionType)
assert type(len) is types.BuiltinFunctionType
assert type([].append) is types.BuiltinFunctionType
assert type([].append).__name__ == "builtin_function_or_method"
assert type(len).__name__ == "builtin_function_or_method"
assert type.__dict__["__dict__"].__objclass__ is type
assert (