From f76e0d16bc54d33a64ea1f74a8a4bb48978d2c15 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 12 Mar 2026 21:15:23 +0900 Subject: [PATCH] Add per-type vectorcall for builtin constructors Add vectorcall fast paths for dict, list, set, int, float, str, bool, tuple, frozenset. Clear vectorcall when __init__/__new__ is overridden in Python. Prevent constructor vectorcall inheritance to heap subclasses. Fix stat_result to use struct_sequence_new with reference-copy for hidden time fields. --- crates/vm/src/builtins/bool.rs | 18 ++++++++++++++ crates/vm/src/builtins/dict.rs | 24 ++++++++++++++++++- crates/vm/src/builtins/float.rs | 13 +++++++++++ crates/vm/src/builtins/int.rs | 18 ++++++++++++++ crates/vm/src/builtins/list.rs | 15 ++++++++++++ crates/vm/src/builtins/set.rs | 40 ++++++++++++++++++++++++++++++++ crates/vm/src/builtins/str.rs | 17 ++++++++++++++ crates/vm/src/builtins/tuple.rs | 20 ++++++++++++++++ crates/vm/src/builtins/type.rs | 24 +++++++++++++++++++ crates/vm/src/stdlib/os.rs | 39 ++++++++++++++----------------- crates/vm/src/types/slot.rs | 8 ++++++- crates/vm/src/types/slot_defs.rs | 23 ++++++++++++++++-- 12 files changed, 233 insertions(+), 26 deletions(-) diff --git a/crates/vm/src/builtins/bool.rs b/crates/vm/src/builtins/bool.rs index db37eee6ed1..2177a890877 100644 --- a/crates/vm/src/builtins/bool.rs +++ b/crates/vm/src/builtins/bool.rs @@ -182,8 +182,26 @@ impl Representable for PyBool { } } +fn vectorcall_bool( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + (zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm) +} + pub(crate) fn init(context: &'static Context) { PyBool::extend_class(context, context.types.bool_type); + context + .types + .bool_type + .slots + .vectorcall + .store(Some(vectorcall_bool)); } // pub fn not(vm: &VirtualMachine, obj: &PyObject) -> PyResult { diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index 0e64e9e66ac..eae4ec7fd6b 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -15,7 +15,9 @@ use crate::{ class::{PyClassDef, PyClassImpl}, common::ascii, dict_inner::{self, DictKey}, - function::{ArgIterable, KwArgs, OptionalArg, PyArithmeticValue::*, PyComparisonValue}, + function::{ + ArgIterable, FuncArgs, KwArgs, OptionalArg, PyArithmeticValue::*, PyComparisonValue, + }, iter::PyExactSizeIterator, protocol::{PyIterIter, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, recursion::ReprGuard, @@ -1433,8 +1435,28 @@ fn set_inner_number_or(a: &PyObject, b: &PyObject, vm: &VirtualMachine) -> PyRes set_inner_number_op(a, b, |a, b| a.union(b, vm), vm) } +fn vectorcall_dict( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let obj = PyDict::default().into_ref_with_type(vm, zelf.to_owned())?; + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + PyDict::slot_init(obj.clone().into(), func_args, vm)?; + Ok(obj.into()) +} + pub(crate) fn init(context: &'static Context) { PyDict::extend_class(context, context.types.dict_type); + context + .types + .dict_type + .slots + .vectorcall + .store(Some(vectorcall_dict)); PyDictKeys::extend_class(context, context.types.dict_keys_type); PyDictKeyIterator::extend_class(context, context.types.dict_keyiterator_type); PyDictReverseKeyIterator::extend_class(context, context.types.dict_reversekeyiterator_type); diff --git a/crates/vm/src/builtins/float.rs b/crates/vm/src/builtins/float.rs index 4d7d7c7101a..2b2f05d8278 100644 --- a/crates/vm/src/builtins/float.rs +++ b/crates/vm/src/builtins/float.rs @@ -525,7 +525,20 @@ pub(crate) fn get_value(obj: &PyObject) -> f64 { obj.downcast_ref::().unwrap().value } +fn vectorcall_float( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + (zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm) +} + #[rustfmt::skip] // to avoid line splitting pub fn init(context: &'static Context) { PyFloat::extend_class(context, context.types.float_type); + context.types.float_type.slots.vectorcall.store(Some(vectorcall_float)); } diff --git a/crates/vm/src/builtins/int.rs b/crates/vm/src/builtins/int.rs index a253506eba1..e42391acd61 100644 --- a/crates/vm/src/builtins/int.rs +++ b/crates/vm/src/builtins/int.rs @@ -791,6 +791,24 @@ pub fn try_to_float(int: &BigInt, vm: &VirtualMachine) -> PyResult { .ok_or_else(|| vm.new_overflow_error("int too large to convert to float")) } +fn vectorcall_int( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + (zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm) +} + pub(crate) fn init(context: &'static Context) { PyInt::extend_class(context, context.types.int_type); + context + .types + .int_type + .slots + .vectorcall + .store(Some(vectorcall_int)); } diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index cdb8a73ead2..06b2469c164 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -768,9 +768,24 @@ impl IterNext for PyListReverseIterator { } } +fn vectorcall_list( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let obj = PyList::default().into_ref_with_type(vm, zelf.to_owned())?; + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + PyList::slot_init(obj.clone().into(), func_args, vm)?; + Ok(obj.into()) +} + pub fn init(context: &'static Context) { let list_type = &context.types.list_type; PyList::extend_class(context, list_type); + list_type.slots.vectorcall.store(Some(vectorcall_list)); PyListIterator::extend_class(context, context.types.list_iterator_type); PyListReverseIterator::extend_class(context, context.types.list_reverseiterator_type); diff --git a/crates/vm/src/builtins/set.rs b/crates/vm/src/builtins/set.rs index 85e6b37fab0..a6a58367f4f 100644 --- a/crates/vm/src/builtins/set.rs +++ b/crates/vm/src/builtins/set.rs @@ -1421,8 +1421,48 @@ impl IterNext for PySetIterator { } } +fn vectorcall_set( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let obj = PySet::default().into_ref_with_type(vm, zelf.to_owned())?; + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + PySet::slot_init(obj.clone().into(), func_args, vm)?; + Ok(obj.into()) +} + +fn vectorcall_frozenset( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + (zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm) +} + pub fn init(context: &'static Context) { PySet::extend_class(context, context.types.set_type); + context + .types + .set_type + .slots + .vectorcall + .store(Some(vectorcall_set)); + PyFrozenSet::extend_class(context, context.types.frozenset_type); + context + .types + .frozenset_type + .slots + .vectorcall + .store(Some(vectorcall_frozenset)); + PySetIterator::extend_class(context, context.types.set_iterator_type); } diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index d882fa913a5..8707c5cf769 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -1781,8 +1781,25 @@ struct ReplaceArgs { count: isize, } +fn vectorcall_str( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + (zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm) +} + pub fn init(ctx: &'static Context) { PyStr::extend_class(ctx, ctx.types.str_type); + ctx.types + .str_type + .slots + .vectorcall + .store(Some(vectorcall_str)); PyStrIterator::extend_class(ctx, ctx.types.str_iterator_type); } diff --git a/crates/vm/src/builtins/tuple.rs b/crates/vm/src/builtins/tuple.rs index 623f7144796..b2c2fabc540 100644 --- a/crates/vm/src/builtins/tuple.rs +++ b/crates/vm/src/builtins/tuple.rs @@ -695,9 +695,29 @@ impl IterNext for PyTupleIterator { } } +fn vectorcall_tuple( + zelf_obj: &PyObject, + args: Vec, + nargs: usize, + kwnames: Option<&[PyObjectRef]>, + vm: &VirtualMachine, +) -> PyResult { + let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); + // Use the type's own slot_new rather than calling PyTuple::slot_new directly, + // so Rust-level subclasses (e.g. struct sequences) get their custom slot_new called. + (zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm) +} + pub(crate) fn init(context: &'static Context) { PyTuple::extend_class(context, context.types.tuple_type); PyTupleIterator::extend_class(context, context.types.tuple_iterator_type); + context + .types + .tuple_type + .slots + .vectorcall + .store(Some(vectorcall_tuple)); } pub(super) fn tuple_hash(elements: &[PyObjectRef], vm: &VirtualMachine) -> PyResult { diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 5970c30fbd3..0f63e7106df 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -2650,6 +2650,24 @@ fn subtype_set_weakref(obj: PyObjectRef, _value: PyObjectRef, vm: &VirtualMachin /// Vectorcall for PyType (PEP 590). /// Fast path: type(x) returns x.__class__ without constructing FuncArgs. +/// +/// # Implementation note: `slots.vectorcall` dual use +/// +/// CPython has three distinct fields on PyTypeObject: +/// - `tp_vectorcall`: constructor fast path (e.g. `list_vectorcall`) +/// - `tp_vectorcall_offset`: per-instance vectorcall for callables (e.g. functions) +/// - `tp_call`: standard call slot +/// +/// RustPython collapses the first two into a single `slots.vectorcall`. The +/// `call.is_none()` guard below distinguishes the two uses: callable types have +/// `slots.call` set, so their `slots.vectorcall` is for calling instances, +/// not for construction. +/// +/// This heuristic is correct for all current builtins but is not a general +/// solution — `type` itself is both callable and has a constructor vectorcall, +/// handled by the explicit `zelf.is(type_type)` check above the guard. +/// If more such types arise, consider splitting into a dedicated +/// `constructor_vectorcall` slot. fn vectorcall_type( zelf_obj: &PyObject, args: Vec, @@ -2665,6 +2683,12 @@ fn vectorcall_type( if nargs == 1 && no_kwargs { return Ok(args[0].obj_type()); } + } else if zelf.slots.call.load().is_none() && zelf.slots.new.load().is_some() { + // Per-type constructor vectorcall for non-callable types (dict, list, int, etc.) + // Also guard on slots.new to avoid dispatching for DISALLOW_INSTANTIATION types. + if let Some(type_vc) = zelf.slots.vectorcall.load() { + return type_vc(zelf_obj, args, nargs, kwnames, vm); + } } // Fallback: construct FuncArgs and use standard call diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 03c5e33de76..ac455ba4dd5 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -178,7 +178,6 @@ pub(super) mod _os { }; use core::time::Duration; use crossbeam_utils::atomic::AtomicCell; - use itertools::Itertools; use rustpython_common::wtf8::Wtf8Buf; use std::{env, fs, fs::OpenOptions, io, path::PathBuf, time::SystemTime}; @@ -1363,29 +1362,25 @@ pub(super) mod _os { #[pyclass(with(PyStructSequence))] impl PyStatResult { #[pyslot] - fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let flatten_args = |r: &[PyObjectRef]| { - let mut vec_args = Vec::from(r); - loop { - if let Ok(obj) = vec_args.iter().exactly_one() { - match obj.downcast_ref::() { - Some(t) => { - vec_args = Vec::from(t.as_slice()); - } - None => { - return vec_args; - } - } - } else { - return vec_args; - } + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let seq: PyObjectRef = args.bind(vm)?; + let result = crate::types::struct_sequence_new(cls.clone(), seq, vm)?; + let tuple = result.downcast_ref::().unwrap(); + let mut items: Vec = tuple.to_vec(); + + // Copy integer time fields to hidden float timestamp slots when not provided. + // indices 7-9: st_atime_int, st_mtime_int, st_ctime_int + // i+3: st_atime/st_mtime/st_ctime (float timestamps, copied from int if missing) + // i+6: st_atime_ns/st_mtime_ns/st_ctime_ns (left as None if not provided) + for i in 7..=9 { + if vm.is_none(&items[i + 3]) { + items[i + 3] = items[i].clone(); } - }; - - let args: FuncArgs = flatten_args(&args.args).into(); + } - let stat: StatResultData = args.bind(vm)?; - Ok(stat.to_pyobject(vm)) + PyTuple::new_unchecked(items.into_boxed_slice()) + .into_ref_with_type(vm, cls) + .map(Into::into) } } diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index 8ffcfd0f3b6..c560e2e1f9e 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -825,11 +825,17 @@ impl PyType { } SlotAccessor::TpIter => update_main_slot!(iter, iter_wrapper, Iter), SlotAccessor::TpIternext => update_main_slot!(iternext, iternext_wrapper, IterNext), - SlotAccessor::TpInit => update_main_slot!(init, init_wrapper, Init), + SlotAccessor::TpInit => { + update_main_slot!(init, init_wrapper, Init); + if ADD { + self.slots.vectorcall.store(None); + } + } SlotAccessor::TpNew => { // __new__ is not wrapped via PyWrapper if ADD { self.slots.new.store(Some(new_wrapper)); + self.slots.vectorcall.store(None); } else { accessor.inherit_from_mro(self); } diff --git a/crates/vm/src/types/slot_defs.rs b/crates/vm/src/types/slot_defs.rs index efbcb9f55f6..efd1a8913f5 100644 --- a/crates/vm/src/types/slot_defs.rs +++ b/crates/vm/src/types/slot_defs.rs @@ -453,7 +453,20 @@ impl SlotAccessor { Self::TpStr => inherit_main!(str), Self::TpCall => { inherit_main!(call); - inherit_main!(vectorcall); + // CPython does not inherit tp_vectorcall to heap types at all; it only + // inherits tp_vectorcall_offset (the per-instance callable fast path). + // RustPython approximates this by inheriting vectorcall only from types + // with a call slot (instance-call vectorcall), not from types that use + // vectorcall as a constructor fast path (call=None). + // See vectorcall_type() in type.rs for the dual-use design rationale. + let inherited_vc = mro.iter().find_map(|cls| { + if cls.slots.call.load().is_some() { + cls.slots.vectorcall.load() + } else { + None + } + }); + typ.slots.vectorcall.store(inherited_vc); } Self::TpIter => inherit_main!(iter), Self::TpIternext => inherit_main!(iternext), @@ -573,7 +586,13 @@ impl SlotAccessor { Self::TpStr => copy_main!(str), Self::TpCall => { copy_main!(call); - copy_main!(vectorcall); + // See inherit_from_mro TpCall for rationale. + if typ.slots.vectorcall.load().is_none() + && base.slots.call.load().is_some() + && let Some(base_val) = base.slots.vectorcall.load() + { + typ.slots.vectorcall.store(Some(base_val)); + } } Self::TpIter => copy_main!(iter), Self::TpIternext => copy_main!(iternext),