Skip to content

vectercall per type #7362

@youknowone

Description

@youknowone

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.rsvectorcall_type dispatch modification
  • crates/vm/src/types/slot.rsupdate_slot vectorcall clearing for TpInit/TpNew
  • crates/vm/src/types/slot_defs.rscopyslot_if_none inheritance (already correct)
  • crates/vm/src/protocol/callable.rsPyCallable::new (no changes needed)
  • Individual builtin type files for vectorcall implementations

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions