Skip to content

Commit 6168704

Browse files
committed
Add per-type vectorcall for builtin constructors (RustPython#7407)
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.
1 parent 5f8bdcc commit 6168704

12 files changed

Lines changed: 233 additions & 26 deletions

File tree

crates/vm/src/builtins/bool.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,26 @@ impl Representable for PyBool {
182182
}
183183
}
184184

185+
fn vectorcall_bool(
186+
zelf_obj: &PyObject,
187+
args: Vec<PyObjectRef>,
188+
nargs: usize,
189+
kwnames: Option<&[PyObjectRef]>,
190+
vm: &VirtualMachine,
191+
) -> PyResult {
192+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
193+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
194+
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
195+
}
196+
185197
pub(crate) fn init(context: &'static Context) {
186198
PyBool::extend_class(context, context.types.bool_type);
199+
context
200+
.types
201+
.bool_type
202+
.slots
203+
.vectorcall
204+
.store(Some(vectorcall_bool));
187205
}
188206

189207
// pub fn not(vm: &VirtualMachine, obj: &PyObject) -> PyResult<bool> {

crates/vm/src/builtins/dict.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use crate::{
1515
class::{PyClassDef, PyClassImpl},
1616
common::ascii,
1717
dict_inner::{self, DictKey},
18-
function::{ArgIterable, KwArgs, OptionalArg, PyArithmeticValue::*, PyComparisonValue},
18+
function::{
19+
ArgIterable, FuncArgs, KwArgs, OptionalArg, PyArithmeticValue::*, PyComparisonValue,
20+
},
1921
iter::PyExactSizeIterator,
2022
protocol::{PyIterIter, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods},
2123
recursion::ReprGuard,
@@ -1433,8 +1435,28 @@ fn set_inner_number_or(a: &PyObject, b: &PyObject, vm: &VirtualMachine) -> PyRes
14331435
set_inner_number_op(a, b, |a, b| a.union(b, vm), vm)
14341436
}
14351437

1438+
fn vectorcall_dict(
1439+
zelf_obj: &PyObject,
1440+
args: Vec<PyObjectRef>,
1441+
nargs: usize,
1442+
kwnames: Option<&[PyObjectRef]>,
1443+
vm: &VirtualMachine,
1444+
) -> PyResult {
1445+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
1446+
let obj = PyDict::default().into_ref_with_type(vm, zelf.to_owned())?;
1447+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
1448+
PyDict::slot_init(obj.clone().into(), func_args, vm)?;
1449+
Ok(obj.into())
1450+
}
1451+
14361452
pub(crate) fn init(context: &'static Context) {
14371453
PyDict::extend_class(context, context.types.dict_type);
1454+
context
1455+
.types
1456+
.dict_type
1457+
.slots
1458+
.vectorcall
1459+
.store(Some(vectorcall_dict));
14381460
PyDictKeys::extend_class(context, context.types.dict_keys_type);
14391461
PyDictKeyIterator::extend_class(context, context.types.dict_keyiterator_type);
14401462
PyDictReverseKeyIterator::extend_class(context, context.types.dict_reversekeyiterator_type);

crates/vm/src/builtins/float.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,20 @@ pub(crate) fn get_value(obj: &PyObject) -> f64 {
525525
obj.downcast_ref::<PyFloat>().unwrap().value
526526
}
527527

528+
fn vectorcall_float(
529+
zelf_obj: &PyObject,
530+
args: Vec<PyObjectRef>,
531+
nargs: usize,
532+
kwnames: Option<&[PyObjectRef]>,
533+
vm: &VirtualMachine,
534+
) -> PyResult {
535+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
536+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
537+
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
538+
}
539+
528540
#[rustfmt::skip] // to avoid line splitting
529541
pub fn init(context: &'static Context) {
530542
PyFloat::extend_class(context, context.types.float_type);
543+
context.types.float_type.slots.vectorcall.store(Some(vectorcall_float));
531544
}

crates/vm/src/builtins/int.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,24 @@ pub fn try_to_float(int: &BigInt, vm: &VirtualMachine) -> PyResult<f64> {
791791
.ok_or_else(|| vm.new_overflow_error("int too large to convert to float"))
792792
}
793793

794+
fn vectorcall_int(
795+
zelf_obj: &PyObject,
796+
args: Vec<PyObjectRef>,
797+
nargs: usize,
798+
kwnames: Option<&[PyObjectRef]>,
799+
vm: &VirtualMachine,
800+
) -> PyResult {
801+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
802+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
803+
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
804+
}
805+
794806
pub(crate) fn init(context: &'static Context) {
795807
PyInt::extend_class(context, context.types.int_type);
808+
context
809+
.types
810+
.int_type
811+
.slots
812+
.vectorcall
813+
.store(Some(vectorcall_int));
796814
}

crates/vm/src/builtins/list.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,9 +768,24 @@ impl IterNext for PyListReverseIterator {
768768
}
769769
}
770770

771+
fn vectorcall_list(
772+
zelf_obj: &PyObject,
773+
args: Vec<PyObjectRef>,
774+
nargs: usize,
775+
kwnames: Option<&[PyObjectRef]>,
776+
vm: &VirtualMachine,
777+
) -> PyResult {
778+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
779+
let obj = PyList::default().into_ref_with_type(vm, zelf.to_owned())?;
780+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
781+
PyList::slot_init(obj.clone().into(), func_args, vm)?;
782+
Ok(obj.into())
783+
}
784+
771785
pub fn init(context: &'static Context) {
772786
let list_type = &context.types.list_type;
773787
PyList::extend_class(context, list_type);
788+
list_type.slots.vectorcall.store(Some(vectorcall_list));
774789

775790
PyListIterator::extend_class(context, context.types.list_iterator_type);
776791
PyListReverseIterator::extend_class(context, context.types.list_reverseiterator_type);

crates/vm/src/builtins/set.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,8 +1421,48 @@ impl IterNext for PySetIterator {
14211421
}
14221422
}
14231423

1424+
fn vectorcall_set(
1425+
zelf_obj: &PyObject,
1426+
args: Vec<PyObjectRef>,
1427+
nargs: usize,
1428+
kwnames: Option<&[PyObjectRef]>,
1429+
vm: &VirtualMachine,
1430+
) -> PyResult {
1431+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
1432+
let obj = PySet::default().into_ref_with_type(vm, zelf.to_owned())?;
1433+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
1434+
PySet::slot_init(obj.clone().into(), func_args, vm)?;
1435+
Ok(obj.into())
1436+
}
1437+
1438+
fn vectorcall_frozenset(
1439+
zelf_obj: &PyObject,
1440+
args: Vec<PyObjectRef>,
1441+
nargs: usize,
1442+
kwnames: Option<&[PyObjectRef]>,
1443+
vm: &VirtualMachine,
1444+
) -> PyResult {
1445+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
1446+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
1447+
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
1448+
}
1449+
14241450
pub fn init(context: &'static Context) {
14251451
PySet::extend_class(context, context.types.set_type);
1452+
context
1453+
.types
1454+
.set_type
1455+
.slots
1456+
.vectorcall
1457+
.store(Some(vectorcall_set));
1458+
14261459
PyFrozenSet::extend_class(context, context.types.frozenset_type);
1460+
context
1461+
.types
1462+
.frozenset_type
1463+
.slots
1464+
.vectorcall
1465+
.store(Some(vectorcall_frozenset));
1466+
14271467
PySetIterator::extend_class(context, context.types.set_iterator_type);
14281468
}

crates/vm/src/builtins/str.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,8 +1781,25 @@ struct ReplaceArgs {
17811781
count: isize,
17821782
}
17831783

1784+
fn vectorcall_str(
1785+
zelf_obj: &PyObject,
1786+
args: Vec<PyObjectRef>,
1787+
nargs: usize,
1788+
kwnames: Option<&[PyObjectRef]>,
1789+
vm: &VirtualMachine,
1790+
) -> PyResult {
1791+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
1792+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
1793+
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
1794+
}
1795+
17841796
pub fn init(ctx: &'static Context) {
17851797
PyStr::extend_class(ctx, ctx.types.str_type);
1798+
ctx.types
1799+
.str_type
1800+
.slots
1801+
.vectorcall
1802+
.store(Some(vectorcall_str));
17861803

17871804
PyStrIterator::extend_class(ctx, ctx.types.str_iterator_type);
17881805
}

crates/vm/src/builtins/tuple.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,9 +695,29 @@ impl IterNext for PyTupleIterator {
695695
}
696696
}
697697

698+
fn vectorcall_tuple(
699+
zelf_obj: &PyObject,
700+
args: Vec<PyObjectRef>,
701+
nargs: usize,
702+
kwnames: Option<&[PyObjectRef]>,
703+
vm: &VirtualMachine,
704+
) -> PyResult {
705+
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
706+
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
707+
// Use the type's own slot_new rather than calling PyTuple::slot_new directly,
708+
// so Rust-level subclasses (e.g. struct sequences) get their custom slot_new called.
709+
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
710+
}
711+
698712
pub(crate) fn init(context: &'static Context) {
699713
PyTuple::extend_class(context, context.types.tuple_type);
700714
PyTupleIterator::extend_class(context, context.types.tuple_iterator_type);
715+
context
716+
.types
717+
.tuple_type
718+
.slots
719+
.vectorcall
720+
.store(Some(vectorcall_tuple));
701721
}
702722

703723
pub(super) fn tuple_hash(elements: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<PyHash> {

crates/vm/src/builtins/type.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2650,6 +2650,24 @@ fn subtype_set_weakref(obj: PyObjectRef, _value: PyObjectRef, vm: &VirtualMachin
26502650

26512651
/// Vectorcall for PyType (PEP 590).
26522652
/// Fast path: type(x) returns x.__class__ without constructing FuncArgs.
2653+
///
2654+
/// # Implementation note: `slots.vectorcall` dual use
2655+
///
2656+
/// CPython has three distinct fields on PyTypeObject:
2657+
/// - `tp_vectorcall`: constructor fast path (e.g. `list_vectorcall`)
2658+
/// - `tp_vectorcall_offset`: per-instance vectorcall for callables (e.g. functions)
2659+
/// - `tp_call`: standard call slot
2660+
///
2661+
/// RustPython collapses the first two into a single `slots.vectorcall`. The
2662+
/// `call.is_none()` guard below distinguishes the two uses: callable types have
2663+
/// `slots.call` set, so their `slots.vectorcall` is for calling instances,
2664+
/// not for construction.
2665+
///
2666+
/// This heuristic is correct for all current builtins but is not a general
2667+
/// solution — `type` itself is both callable and has a constructor vectorcall,
2668+
/// handled by the explicit `zelf.is(type_type)` check above the guard.
2669+
/// If more such types arise, consider splitting into a dedicated
2670+
/// `constructor_vectorcall` slot.
26532671
fn vectorcall_type(
26542672
zelf_obj: &PyObject,
26552673
args: Vec<PyObjectRef>,
@@ -2665,6 +2683,12 @@ fn vectorcall_type(
26652683
if nargs == 1 && no_kwargs {
26662684
return Ok(args[0].obj_type());
26672685
}
2686+
} else if zelf.slots.call.load().is_none() && zelf.slots.new.load().is_some() {
2687+
// Per-type constructor vectorcall for non-callable types (dict, list, int, etc.)
2688+
// Also guard on slots.new to avoid dispatching for DISALLOW_INSTANTIATION types.
2689+
if let Some(type_vc) = zelf.slots.vectorcall.load() {
2690+
return type_vc(zelf_obj, args, nargs, kwnames, vm);
2691+
}
26682692
}
26692693

26702694
// Fallback: construct FuncArgs and use standard call

crates/vm/src/stdlib/os.rs

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ pub(super) mod _os {
178178
};
179179
use core::time::Duration;
180180
use crossbeam_utils::atomic::AtomicCell;
181-
use itertools::Itertools;
182181
use rustpython_common::wtf8::Wtf8Buf;
183182
use std::{env, fs, fs::OpenOptions, io, path::PathBuf, time::SystemTime};
184183

@@ -1363,29 +1362,25 @@ pub(super) mod _os {
13631362
#[pyclass(with(PyStructSequence))]
13641363
impl PyStatResult {
13651364
#[pyslot]
1366-
fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
1367-
let flatten_args = |r: &[PyObjectRef]| {
1368-
let mut vec_args = Vec::from(r);
1369-
loop {
1370-
if let Ok(obj) = vec_args.iter().exactly_one() {
1371-
match obj.downcast_ref::<PyTuple>() {
1372-
Some(t) => {
1373-
vec_args = Vec::from(t.as_slice());
1374-
}
1375-
None => {
1376-
return vec_args;
1377-
}
1378-
}
1379-
} else {
1380-
return vec_args;
1381-
}
1365+
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
1366+
let seq: PyObjectRef = args.bind(vm)?;
1367+
let result = crate::types::struct_sequence_new(cls.clone(), seq, vm)?;
1368+
let tuple = result.downcast_ref::<PyTuple>().unwrap();
1369+
let mut items: Vec<PyObjectRef> = tuple.to_vec();
1370+
1371+
// Copy integer time fields to hidden float timestamp slots when not provided.
1372+
// indices 7-9: st_atime_int, st_mtime_int, st_ctime_int
1373+
// i+3: st_atime/st_mtime/st_ctime (float timestamps, copied from int if missing)
1374+
// i+6: st_atime_ns/st_mtime_ns/st_ctime_ns (left as None if not provided)
1375+
for i in 7..=9 {
1376+
if vm.is_none(&items[i + 3]) {
1377+
items[i + 3] = items[i].clone();
13821378
}
1383-
};
1384-
1385-
let args: FuncArgs = flatten_args(&args.args).into();
1379+
}
13861380

1387-
let stat: StatResultData = args.bind(vm)?;
1388-
Ok(stat.to_pyobject(vm))
1381+
PyTuple::new_unchecked(items.into_boxed_slice())
1382+
.into_ref_with_type(vm, cls)
1383+
.map(Into::into)
13891384
}
13901385
}
13911386

0 commit comments

Comments
 (0)