Skip to content

Commit 947e149

Browse files
cmaloneyclaude
andcommitted
gh-87613: Argument Cliic @vectorcall decorator
Add `@vectorcall` as a decorator to Argument Clinic (AC) which generates a new [Vectorcall Protocol](https://docs.python.org/3/c-api/call.html#the-vectorcall-protocol) argument parsing C function named `{}_vectorcall`. This is only supported for `__new__` and `__init__` currently to simplify implementation. The generated code has similar or better performance to existing hand-written cases for `list`, `float`, `str`, `tuple`, `enumerate`, `reversed`, and `int`. Using the decorator added vectorcall to `bytearray` and construction got 1.09x faster. For more details see the comments in gh-87613. The `@vectorcall` decorator has two options: - **zero_arg={C_FUNC}**: Some types, like `int`, can be called with zero arguments and return an immortal object in that case. Adding a shortcut is needed to match existing hand-written performance; provides an over 10% performance change for those cases. - **exact_only**: If the type is not an exact match delegate to the existing non-vectorcall implementation. NEeded for `str` to get matching performance while ensuring correct behavior. Implementation details: - Adds support for the new decorator with arguments in the AC DSL Parser - Move keyword argument parsing generation from inline to a function so both vectorcall, `vc_`, and existing can share code generation. - Adds an `emit` helper to simplify code a bit from existing AC cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3484ef6 commit 947e149

File tree

6 files changed

+620
-66
lines changed

6 files changed

+620
-66
lines changed

Lib/test/test_clinic.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ def test_directive_output_invalid_command(self):
626626
- 'impl_prototype'
627627
- 'parser_prototype'
628628
- 'parser_definition'
629+
- 'vectorcall_definition'
629630
- 'cpp_endif'
630631
- 'methoddef_ifndef'
631632
- 'impl_definition'
@@ -2496,6 +2497,162 @@ def test_duplicate_coexist(self):
24962497
"""
24972498
self.expect_failure(block, err, lineno=2)
24982499

2500+
def test_duplicate_vectorcall(self):
2501+
err = "Called @vectorcall twice"
2502+
block = """
2503+
module m
2504+
class Foo "FooObject *" ""
2505+
@vectorcall
2506+
@vectorcall
2507+
Foo.__init__
2508+
"""
2509+
self.expect_failure(block, err, lineno=3)
2510+
2511+
def test_vectorcall_on_regular_method(self):
2512+
err = "@vectorcall can only be used with __init__ and __new__ methods"
2513+
block = """
2514+
module m
2515+
class Foo "FooObject *" ""
2516+
@vectorcall
2517+
Foo.some_method
2518+
"""
2519+
self.expect_failure(block, err, lineno=3)
2520+
2521+
def test_vectorcall_on_module_function(self):
2522+
err = "@vectorcall can only be used with __init__ and __new__ methods"
2523+
block = """
2524+
module m
2525+
@vectorcall
2526+
m.fn
2527+
"""
2528+
self.expect_failure(block, err, lineno=2)
2529+
2530+
def test_vectorcall_on_init(self):
2531+
block = """
2532+
module m
2533+
class Foo "FooObject *" "Foo_Type"
2534+
@vectorcall
2535+
Foo.__init__
2536+
iterable: object = NULL
2537+
/
2538+
"""
2539+
func = self.parse_function(block, signatures_in_block=3,
2540+
function_index=2)
2541+
self.assertTrue(func.vectorcall)
2542+
self.assertFalse(func.vectorcall_exact_only)
2543+
2544+
def test_vectorcall_on_new(self):
2545+
block = """
2546+
module m
2547+
class Foo "FooObject *" "Foo_Type"
2548+
@classmethod
2549+
@vectorcall
2550+
Foo.__new__
2551+
x: object = NULL
2552+
/
2553+
"""
2554+
func = self.parse_function(block, signatures_in_block=3,
2555+
function_index=2)
2556+
self.assertTrue(func.vectorcall)
2557+
self.assertFalse(func.vectorcall_exact_only)
2558+
2559+
def test_vectorcall_exact_only(self):
2560+
block = """
2561+
module m
2562+
class Foo "FooObject *" "Foo_Type"
2563+
@vectorcall exact_only
2564+
Foo.__init__
2565+
iterable: object = NULL
2566+
/
2567+
"""
2568+
func = self.parse_function(block, signatures_in_block=3,
2569+
function_index=2)
2570+
self.assertTrue(func.vectorcall)
2571+
self.assertTrue(func.vectorcall_exact_only)
2572+
2573+
def test_vectorcall_init_with_kwargs(self):
2574+
block = """
2575+
module m
2576+
class Foo "FooObject *" "Foo_Type"
2577+
@vectorcall
2578+
Foo.__init__
2579+
source: object = NULL
2580+
encoding: str = NULL
2581+
errors: str = NULL
2582+
"""
2583+
func = self.parse_function(block, signatures_in_block=3,
2584+
function_index=2)
2585+
self.assertTrue(func.vectorcall)
2586+
2587+
def test_vectorcall_new_with_kwargs(self):
2588+
block = """
2589+
module m
2590+
class Foo "FooObject *" "Foo_Type"
2591+
@classmethod
2592+
@vectorcall
2593+
Foo.__new__
2594+
source: object = NULL
2595+
*
2596+
encoding: str = NULL
2597+
errors: str = NULL
2598+
"""
2599+
func = self.parse_function(block, signatures_in_block=3,
2600+
function_index=2)
2601+
self.assertTrue(func.vectorcall)
2602+
2603+
def test_vectorcall_init_no_args(self):
2604+
block = """
2605+
module m
2606+
class Foo "FooObject *" "Foo_Type"
2607+
@vectorcall
2608+
Foo.__init__
2609+
"""
2610+
func = self.parse_function(block, signatures_in_block=3,
2611+
function_index=2)
2612+
self.assertTrue(func.vectorcall)
2613+
2614+
def test_vectorcall_zero_arg(self):
2615+
block = """
2616+
module m
2617+
class Foo "FooObject *" "Foo_Type"
2618+
@classmethod
2619+
@vectorcall zero_arg=_PyFoo_GetEmpty()
2620+
Foo.__new__
2621+
x: object = NULL
2622+
/
2623+
"""
2624+
func = self.parse_function(block, signatures_in_block=3,
2625+
function_index=2)
2626+
self.assertTrue(func.vectorcall)
2627+
self.assertFalse(func.vectorcall_exact_only)
2628+
self.assertEqual(func.vectorcall_zero_arg, '_PyFoo_GetEmpty()')
2629+
2630+
def test_vectorcall_zero_arg_with_exact(self):
2631+
block = """
2632+
module m
2633+
class Foo "FooObject *" "Foo_Type"
2634+
@classmethod
2635+
@vectorcall exact_only zero_arg=get_cached()
2636+
Foo.__new__
2637+
x: object = NULL
2638+
/
2639+
"""
2640+
func = self.parse_function(block, signatures_in_block=3,
2641+
function_index=2)
2642+
self.assertTrue(func.vectorcall)
2643+
self.assertTrue(func.vectorcall_exact_only)
2644+
self.assertEqual(func.vectorcall_zero_arg, 'get_cached()')
2645+
2646+
def test_vectorcall_invalid_kwarg(self):
2647+
err = "unknown argument"
2648+
block = """
2649+
module m
2650+
class Foo "FooObject *" ""
2651+
@vectorcall bogus=True
2652+
Foo.__init__
2653+
"""
2654+
self.expect_failure(block, err, lineno=2)
2655+
24992656
def test_unused_param(self):
25002657
block = self.parse("""
25012658
module foo

Tools/clinic/libclinic/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def __init__(
121121
'impl_prototype': d('file'),
122122
'parser_prototype': d('suppress'),
123123
'parser_definition': d('file'),
124+
'vectorcall_definition': d('file'),
124125
'cpp_endif': d('file'),
125126
'methoddef_ifndef': d('file', 1),
126127
'impl_definition': d('block'),

Tools/clinic/libclinic/clanguage.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from libclinic.function import (
1515
Module, Class, Function, Parameter,
1616
permute_optional_groups,
17-
GETTER, SETTER, METHOD_INIT)
17+
GETTER, SETTER, METHOD_INIT, METHOD_NEW)
1818
from libclinic.converters import self_converter
1919
from libclinic.parse_args import ParseArgsCodeGen
2020
if TYPE_CHECKING:
@@ -478,6 +478,24 @@ def render_function(
478478
template_dict['parser_parameters'] = ", ".join(data.impl_parameters[1:])
479479
template_dict['impl_arguments'] = ", ".join(data.impl_arguments)
480480

481+
# Vectorcall impl arguments: replace self/type with the appropriate
482+
# expression for the vectorcall calling convention.
483+
if f.vectorcall and f.cls:
484+
if f.kind is METHOD_INIT:
485+
# For __init__: self is a locally-allocated PyObject*
486+
vc_first = f"({f.cls.typedef})self"
487+
elif f.kind is METHOD_NEW:
488+
# For __new__: type is PyObject* in vectorcall, need cast
489+
vc_first = "_PyType_CAST(type)"
490+
else:
491+
raise AssertionError(
492+
f"Unhandled function kind for vectorcall: {f.kind!r}"
493+
)
494+
vc_impl_args = [vc_first] + data.impl_arguments[1:]
495+
template_dict['vc_impl_arguments'] = ", ".join(vc_impl_args)
496+
else:
497+
template_dict['vc_impl_arguments'] = ""
498+
481499
template_dict['return_conversion'] = libclinic.format_escape("".join(data.return_conversion).rstrip())
482500
template_dict['post_parsing'] = libclinic.format_escape("".join(data.post_parsing).rstrip())
483501
template_dict['cleanup'] = libclinic.format_escape("".join(data.cleanup))

Tools/clinic/libclinic/dsl_parser.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ def reset(self) -> None:
302302
self.critical_section = False
303303
self.target_critical_section = []
304304
self.disable_fastcall = False
305+
self.vectorcall = False
306+
self.vectorcall_exact_only = False
307+
self.vectorcall_zero_arg = ''
305308
self.permit_long_summary = False
306309
self.permit_long_docstring_body = False
307310

@@ -466,6 +469,24 @@ def at_staticmethod(self) -> None:
466469
fail("Can't set @staticmethod, function is not a normal callable")
467470
self.kind = STATIC_METHOD
468471

472+
def at_vectorcall(self, *args: str) -> None:
473+
if self.vectorcall:
474+
fail("Called @vectorcall twice!")
475+
self.vectorcall = True
476+
for arg in args:
477+
if '=' in arg:
478+
key, value = arg.split('=', 1)
479+
else:
480+
key, value = arg, ''
481+
if key == 'exact_only':
482+
self.vectorcall_exact_only = True
483+
elif key == 'zero_arg':
484+
if not value:
485+
fail("@vectorcall zero_arg requires a value")
486+
self.vectorcall_zero_arg = value
487+
else:
488+
fail(f"@vectorcall: unknown argument {key!r}")
489+
469490
def at_coexist(self) -> None:
470491
if self.coexist:
471492
fail("Called @coexist twice!")
@@ -599,6 +620,10 @@ def normalize_function_kind(self, fullname: str) -> None:
599620
elif name == '__init__':
600621
self.kind = METHOD_INIT
601622

623+
# Validate @vectorcall usage.
624+
if self.vectorcall and not self.kind.new_or_init:
625+
fail("@vectorcall can only be used with __init__ and __new__ methods currently")
626+
602627
def resolve_return_converter(
603628
self, full_name: str, forced_converter: str
604629
) -> CReturnConverter:
@@ -723,7 +748,10 @@ def state_modulename_name(self, line: str) -> None:
723748
critical_section=self.critical_section,
724749
disable_fastcall=self.disable_fastcall,
725750
target_critical_section=self.target_critical_section,
726-
forced_text_signature=self.forced_text_signature
751+
forced_text_signature=self.forced_text_signature,
752+
vectorcall=self.vectorcall,
753+
vectorcall_exact_only=self.vectorcall_exact_only,
754+
vectorcall_zero_arg=self.vectorcall_zero_arg,
727755
)
728756
self.add_function(func)
729757

Tools/clinic/libclinic/function.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ class Function:
111111
critical_section: bool = False
112112
disable_fastcall: bool = False
113113
target_critical_section: list[str] = dc.field(default_factory=list)
114+
vectorcall: bool = False
115+
vectorcall_exact_only: bool = False
116+
vectorcall_zero_arg: str = ''
114117

115118
def __post_init__(self) -> None:
116119
self.parent = self.cls or self.module

0 commit comments

Comments
 (0)