Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
builtin_function_or_method
  • Loading branch information
youknowone committed Feb 8, 2026
commit 2a30d0469dee2ab238ef77e2ce8978bb5cc554f7
42 changes: 12 additions & 30 deletions crates/vm/src/builtins/builtin_func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
use alloc::fmt;

// PyCFunctionObject in CPython
#[repr(C)]
#[pyclass(name = "builtin_function_or_method", module = false)]
pub struct PyNativeFunction {
pub(crate) value: &'static PyMethodDef,
Expand Down Expand Up @@ -146,7 +147,9 @@ impl Representable for PyNativeFunction {
}

// `PyCMethodObject` in CPython
#[pyclass(name = "builtin_method", module = false, base = PyNativeFunction, ctx = "builtin_method_type")]
// repr(C) ensures `func` is at offset 0, allowing safe cast from PyNativeMethod to PyNativeFunction
#[repr(C)]
#[pyclass(name = "builtin_function_or_method", module = false, base = PyNativeFunction, ctx = "builtin_function_or_method_type")]
pub struct PyNativeMethod {
pub(crate) func: PyNativeFunction,
pub(crate) class: &'static Py<PyType>, // TODO: the actual life is &'self
Expand All @@ -157,34 +160,8 @@ pub struct PyNativeMethod {
flags(HAS_DICT, DISALLOW_INSTANTIATION)
)]
impl PyNativeMethod {
#[pygetset]
fn __qualname__(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
let prefix = zelf.class.name().to_string();
Ok(vm
.ctx
.new_str(format!("{}.{}", prefix, &zelf.func.value.name)))
}

#[pymethod]
fn __reduce__(
&self,
vm: &VirtualMachine,
) -> PyResult<(PyObjectRef, (PyObjectRef, &'static str))> {
// TODO: return (getattr, (self.object, self.name)) if this is a method
let getattr = vm.builtins.get_attr("getattr", vm)?;
let target = self
.func
.zelf
.clone()
.unwrap_or_else(|| self.class.to_owned().into());
let name = self.func.value.name;
Ok((getattr, (target, name)))
}

#[pygetset]
fn __self__(zelf: PyRef<Self>, _vm: &VirtualMachine) -> Option<PyObjectRef> {
zelf.func.zelf.clone()
}
// __qualname__, __self__, and __reduce__ are inherited from PyNativeFunction
// via NativeFunctionOrMethod wrapper since we share the same Python type.
}

impl fmt::Debug for PyNativeMethod {
Expand Down Expand Up @@ -246,15 +223,20 @@ impl Representable for PyNativeMethod {

pub fn init(context: &Context) {
PyNativeFunction::extend_class(context, context.types.builtin_function_or_method_type);
PyNativeMethod::extend_class(context, context.types.builtin_method_type);
PyNativeMethod::extend_class(context, context.types.builtin_function_or_method_type);
}

/// Wrapper that provides access to the common PyNativeFunction data
/// for both PyNativeFunction and PyNativeMethod (which has func as its first field).
struct NativeFunctionOrMethod(PyRef<PyNativeFunction>);

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.
Ok(Self(unsafe { obj.downcast_unchecked() }))
} else {
Err(vm.new_downcast_type_error(class, &obj))
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/function/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ impl PyMethodDef {
) -> PyRef<PyNativeMethod> {
PyRef::new_ref(
self.to_bound_method(obj, class),
ctx.types.builtin_method_type.to_owned(),
ctx.types.builtin_function_or_method_type.to_owned(),
None,
)
}
Expand Down
8 changes: 7 additions & 1 deletion crates/vm/src/stdlib/_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! This module exposes built-in types that are used by the `types` module.

pub(crate) use _types::make_module;
pub(crate) use _types::module_def;

#[pymodule]
#[allow(non_snake_case)]
Expand Down Expand Up @@ -99,6 +99,12 @@ mod _types {
vm.ctx.types.method_descriptor_type.to_owned().into()
}

#[pyattr]
fn ClassMethodDescriptorType(vm: &VirtualMachine) -> PyObjectRef {
// TODO: implement as separate type
vm.ctx.types.method_descriptor_type.to_owned().into()
}
Comment on lines +107 to +111
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.

⚠️ Potential issue | 🟠 Major

ClassMethodDescriptorType should not alias MethodDescriptorType.

On Line 109, this TODO means types.ClassMethodDescriptorType becomes identical to types.MethodDescriptorType, which diverges from CPython and can break code that relies on type identity checks. Please introduce a dedicated type (or avoid exposing this attribute until it exists).

🤖 Prompt for AI Agents
In `@crates/vm/src/stdlib/_types.rs` around lines 107 - 111, The
ClassMethodDescriptorType function currently returns
vm.ctx.types.method_descriptor_type, causing types.ClassMethodDescriptorType to
be identical to MethodDescriptorType; instead either register and return a
distinct type object for class method descriptors (e.g. create and expose a new
vm.ctx.types.class_method_descriptor_type via the same type-registration flow
used for other descriptor types and return that from ClassMethodDescriptorType)
or remove/withhold the pyattr until the dedicated type is implemented; update
the ClassMethodDescriptorType implementation to return the new unique type
symbol (class_method_descriptor_type) rather than method_descriptor_type so type
identity matches CPython.


#[pyattr]
fn MethodType(vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.types.bound_method_type.to_owned().into()
Expand Down
22 changes: 15 additions & 7 deletions crates/vm/src/types/zoo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,20 @@ impl TypeZoo {
#[cold]
pub(crate) fn init() -> Self {
let (type_type, object_type, weakref_type) = crate::object::init_type_hierarchy();
// the order matters for type, object, weakref, and int - must be initialized first
let type_type = type_::PyType::init_manually(type_type);
let object_type = object::PyBaseObject::init_manually(object_type);
let weakref_type = weakref::PyWeak::init_manually(weakref_type);
let int_type = int::PyInt::init_builtin_type();

// builtin_function_or_method and builtin_method share the same type (CPython behavior)
let builtin_function_or_method_type = builtin_func::PyNativeFunction::init_builtin_type();

Self {
// the order matters for type, object, weakref, and int
type_type: type_::PyType::init_manually(type_type),
object_type: object::PyBaseObject::init_manually(object_type),
weakref_type: weakref::PyWeak::init_manually(weakref_type),
int_type: int::PyInt::init_builtin_type(),
type_type,
object_type,
weakref_type,
int_type,

// types exposed as builtins
bool_type: bool_::PyBool::init_builtin_type(),
Expand Down Expand Up @@ -147,8 +155,8 @@ impl TypeZoo {
asyncgenerator::PyAsyncGenWrappedValue::init_builtin_type(),
anext_awaitable: asyncgenerator::PyAnextAwaitable::init_builtin_type(),
bound_method_type: function::PyBoundMethod::init_builtin_type(),
builtin_function_or_method_type: builtin_func::PyNativeFunction::init_builtin_type(),
builtin_method_type: builtin_func::PyNativeMethod::init_builtin_type(),
builtin_function_or_method_type,
builtin_method_type: builtin_function_or_method_type,
bytearray_iterator_type: bytearray::PyByteArrayIterator::init_builtin_type(),
bytes_iterator_type: bytes::PyBytesIterator::init_builtin_type(),
callable_iterator: iter::PyCallableIterator::init_builtin_type(),
Expand Down