Support per-type-instance vectorcall for builtin types (dict, list, int, ...)
Summary
Currently, all type-object constructor calls (e.g. dict(x=1), list([1,2,3]), int("42")) go through a single vectorcall_type function, which only has a fast path for type(x) and falls back to the generic PyType::call slow path for everything else. This means every builtin type constructor pays the cost of slot_new(args.clone()) + slot_init(args) dispatch, including an unnecessary args.clone().
CPython avoids this by giving each PyTypeObject its own tp_vectorcall function pointer. When you call dict(...), CPython reads PyDict_Type.tp_vectorcall (= dict_vectorcall) directly, bypassing the generic type.__call__ → __new__ + __init__ chain entirely. Over 15 builtin types have dedicated vectorcall implementations.
Current Architecture in RustPython
dict(x=1)
→ PyCallable::new(dict_type_obj)
→ obj.class() = type (metatype)
→ type.slots.vectorcall = vectorcall_type
→ vectorcall_type: not type(x), so fallback
→ PyType::call(dict_type, args)
→ slot_new(args.clone()) ← unnecessary clone
→ slot_init(args)
Each PyType instance already has its own slots: PyTypeSlots with vectorcall: AtomicCell<Option<VectorCallFunc>>, but the dispatch path never reads it. vectorcall_type receives the type object as its first argument but ignores the type's own slots.vectorcall.
Proposed Solution
1. Modify vectorcall_type to dispatch per-type vectorcall
In crates/vm/src/builtins/type.rs, add a branch that checks the called type's own slots.vectorcall:
fn vectorcall_type(...) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
if zelf.is(vm.ctx.types.type_type) {
// type(x) fast path (existing)
...
} else if let Some(type_vc) = zelf.slots.vectorcall.load() {
// Per-type vectorcall: dict_vectorcall, list_vectorcall, etc.
return type_vc(zelf_obj, args, nargs, kwnames, vm);
}
// Fallback to PyType::call
...
}
The else if structure prevents infinite recursion: when zelf is type_type itself, we never read zelf.slots.vectorcall (which would be vectorcall_type again).
2. Clear vectorcall on __init__/__new__ override
In crates/vm/src/types/slot.rs, when update_slot processes TpInit or TpNew with ADD=true, also clear self.slots.vectorcall.store(None).
This ensures subclasses that override __init__ or __new__ don't inherit an incorrect per-type vectorcall. For example, class MyDict(dict): def __init__(self, ...): ... must NOT use dict_vectorcall, which would skip the Python __init__ override.
3. Implement and register per-type vectorcall functions
Each builtin type gets a dedicated vectorcall function registered in its init():
| Type |
File |
Pattern |
| dict |
builtins/dict.rs |
DefaultConstructor + Initializer (skip slot_new, pass args only to slot_init) |
| list |
builtins/list.rs |
Constructor |
| tuple |
builtins/tuple.rs |
Constructor |
| int |
builtins/int.rs |
Constructor |
| float |
builtins/float.rs |
Constructor |
| str |
builtins/pystr.rs |
Constructor |
| bool |
builtins/bool_.rs |
Constructor |
| set |
builtins/set.rs |
DefaultConstructor + Initializer |
| frozenset |
builtins/set.rs |
Constructor |
The key optimization for DefaultConstructor + Initializer types (like dict, set) is avoiding the args.clone() in PyType::call line 2216 — since Default::default() needs no args, we construct the object first, then pass args only to slot_init.
Inheritance Behavior
- Vectorcall is already inherited alongside call via copyslot_if_none in slot_defs.rs (lines 574-577)
- class MyDict(dict): pass → inherits dict_vectorcall ✓
- class MyDict(dict): def __init__(self, ...): ... → vectorcall cleared to None, falls back to PyType::call ✓
- Custom metaclass with __call__ override → vectorcall cleared on metaclass, per-type vectorcall never reached ✓
Key Files
- crates/vm/src/builtins/type.rs — vectorcall_type dispatch modification
- crates/vm/src/types/slot.rs — update_slot vectorcall clearing for TpInit/TpNew
- crates/vm/src/types/slot_defs.rs — copyslot_if_none inheritance (already correct)
- crates/vm/src/protocol/callable.rs — PyCallable::new (no changes needed)
- Individual builtin type files for vectorcall implementations
References
Support per-type-instance vectorcall for builtin types (dict, list, int, ...)
Summary
Currently, all type-object constructor calls (e.g. dict(x=1), list([1,2,3]), int("42")) go through a single vectorcall_type function, which only has a fast path for type(x) and falls back to the generic PyType::call slow path for everything else. This means every builtin type constructor pays the cost of slot_new(args.clone()) + slot_init(args) dispatch, including an unnecessary args.clone().
CPython avoids this by giving each PyTypeObject its own tp_vectorcall function pointer. When you call dict(...), CPython reads PyDict_Type.tp_vectorcall (= dict_vectorcall) directly, bypassing the generic type.__call__ → __new__ + __init__ chain entirely. Over 15 builtin types have dedicated vectorcall implementations.
Current Architecture in RustPython
dict(x=1) → PyCallable::new(dict_type_obj) → obj.class() = type (metatype) → type.slots.vectorcall = vectorcall_type → vectorcall_type: not type(x), so fallback → PyType::call(dict_type, args) → slot_new(args.clone()) ← unnecessary clone → slot_init(args)Each PyType instance already has its own slots: PyTypeSlots with vectorcall: AtomicCell<Option<VectorCallFunc>>, but the dispatch path never reads it. vectorcall_type receives the type object as its first argument but ignores the type's own slots.vectorcall.
Proposed Solution
1. Modify vectorcall_type to dispatch per-type vectorcall
In crates/vm/src/builtins/type.rs, add a branch that checks the called type's own slots.vectorcall:
The else if structure prevents infinite recursion: when zelf is type_type itself, we never read zelf.slots.vectorcall (which would be vectorcall_type again).
2. Clear vectorcall on __init__/__new__ override
In crates/vm/src/types/slot.rs, when update_slot processes TpInit or TpNew with ADD=true, also clear self.slots.vectorcall.store(None).
This ensures subclasses that override __init__ or __new__ don't inherit an incorrect per-type vectorcall. For example, class MyDict(dict): def __init__(self, ...): ... must NOT use dict_vectorcall, which would skip the Python __init__ override.
3. Implement and register per-type vectorcall functions
Each builtin type gets a dedicated vectorcall function registered in its init():
The key optimization for DefaultConstructor + Initializer types (like dict, set) is avoiding the args.clone() in PyType::call line 2216 — since Default::default() needs no args, we construct the object first, then pass args only to slot_init.
Inheritance Behavior
Key Files
References