From 947e149a93f55dfb4f4199dd7ef56d193e8ba38d Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sat, 21 Feb 2026 20:26:38 -0800 Subject: [PATCH 01/20] 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 --- Lib/test/test_clinic.py | 157 +++++++++ Tools/clinic/libclinic/app.py | 1 + Tools/clinic/libclinic/clanguage.py | 20 +- Tools/clinic/libclinic/dsl_parser.py | 30 +- Tools/clinic/libclinic/function.py | 3 + Tools/clinic/libclinic/parse_args.py | 475 +++++++++++++++++++++++---- 6 files changed, 620 insertions(+), 66 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index e71f9fc181bb43..def49c970e5467 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -626,6 +626,7 @@ def test_directive_output_invalid_command(self): - 'impl_prototype' - 'parser_prototype' - 'parser_definition' + - 'vectorcall_definition' - 'cpp_endif' - 'methoddef_ifndef' - 'impl_definition' @@ -2496,6 +2497,162 @@ def test_duplicate_coexist(self): """ self.expect_failure(block, err, lineno=2) + def test_duplicate_vectorcall(self): + err = "Called @vectorcall twice" + block = """ + module m + class Foo "FooObject *" "" + @vectorcall + @vectorcall + Foo.__init__ + """ + self.expect_failure(block, err, lineno=3) + + def test_vectorcall_on_regular_method(self): + err = "@vectorcall can only be used with __init__ and __new__ methods" + block = """ + module m + class Foo "FooObject *" "" + @vectorcall + Foo.some_method + """ + self.expect_failure(block, err, lineno=3) + + def test_vectorcall_on_module_function(self): + err = "@vectorcall can only be used with __init__ and __new__ methods" + block = """ + module m + @vectorcall + m.fn + """ + self.expect_failure(block, err, lineno=2) + + def test_vectorcall_on_init(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @vectorcall + Foo.__init__ + iterable: object = NULL + / + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + self.assertFalse(func.vectorcall_exact_only) + + def test_vectorcall_on_new(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @classmethod + @vectorcall + Foo.__new__ + x: object = NULL + / + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + self.assertFalse(func.vectorcall_exact_only) + + def test_vectorcall_exact_only(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @vectorcall exact_only + Foo.__init__ + iterable: object = NULL + / + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + self.assertTrue(func.vectorcall_exact_only) + + def test_vectorcall_init_with_kwargs(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @vectorcall + Foo.__init__ + source: object = NULL + encoding: str = NULL + errors: str = NULL + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + + def test_vectorcall_new_with_kwargs(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @classmethod + @vectorcall + Foo.__new__ + source: object = NULL + * + encoding: str = NULL + errors: str = NULL + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + + def test_vectorcall_init_no_args(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @vectorcall + Foo.__init__ + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + + def test_vectorcall_zero_arg(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @classmethod + @vectorcall zero_arg=_PyFoo_GetEmpty() + Foo.__new__ + x: object = NULL + / + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + self.assertFalse(func.vectorcall_exact_only) + self.assertEqual(func.vectorcall_zero_arg, '_PyFoo_GetEmpty()') + + def test_vectorcall_zero_arg_with_exact(self): + block = """ + module m + class Foo "FooObject *" "Foo_Type" + @classmethod + @vectorcall exact_only zero_arg=get_cached() + Foo.__new__ + x: object = NULL + / + """ + func = self.parse_function(block, signatures_in_block=3, + function_index=2) + self.assertTrue(func.vectorcall) + self.assertTrue(func.vectorcall_exact_only) + self.assertEqual(func.vectorcall_zero_arg, 'get_cached()') + + def test_vectorcall_invalid_kwarg(self): + err = "unknown argument" + block = """ + module m + class Foo "FooObject *" "" + @vectorcall bogus=True + Foo.__init__ + """ + self.expect_failure(block, err, lineno=2) + def test_unused_param(self): block = self.parse(""" module foo diff --git a/Tools/clinic/libclinic/app.py b/Tools/clinic/libclinic/app.py index 9e8cec5320f877..c8ca4cb452a0e9 100644 --- a/Tools/clinic/libclinic/app.py +++ b/Tools/clinic/libclinic/app.py @@ -121,6 +121,7 @@ def __init__( 'impl_prototype': d('file'), 'parser_prototype': d('suppress'), 'parser_definition': d('file'), + 'vectorcall_definition': d('file'), 'cpp_endif': d('file'), 'methoddef_ifndef': d('file', 1), 'impl_definition': d('block'), diff --git a/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py index 9e7fa7a7f58f95..8b59b5367ddb23 100644 --- a/Tools/clinic/libclinic/clanguage.py +++ b/Tools/clinic/libclinic/clanguage.py @@ -14,7 +14,7 @@ from libclinic.function import ( Module, Class, Function, Parameter, permute_optional_groups, - GETTER, SETTER, METHOD_INIT) + GETTER, SETTER, METHOD_INIT, METHOD_NEW) from libclinic.converters import self_converter from libclinic.parse_args import ParseArgsCodeGen if TYPE_CHECKING: @@ -478,6 +478,24 @@ def render_function( template_dict['parser_parameters'] = ", ".join(data.impl_parameters[1:]) template_dict['impl_arguments'] = ", ".join(data.impl_arguments) + # Vectorcall impl arguments: replace self/type with the appropriate + # expression for the vectorcall calling convention. + if f.vectorcall and f.cls: + if f.kind is METHOD_INIT: + # For __init__: self is a locally-allocated PyObject* + vc_first = f"({f.cls.typedef})self" + elif f.kind is METHOD_NEW: + # For __new__: type is PyObject* in vectorcall, need cast + vc_first = "_PyType_CAST(type)" + else: + raise AssertionError( + f"Unhandled function kind for vectorcall: {f.kind!r}" + ) + vc_impl_args = [vc_first] + data.impl_arguments[1:] + template_dict['vc_impl_arguments'] = ", ".join(vc_impl_args) + else: + template_dict['vc_impl_arguments'] = "" + template_dict['return_conversion'] = libclinic.format_escape("".join(data.return_conversion).rstrip()) template_dict['post_parsing'] = libclinic.format_escape("".join(data.post_parsing).rstrip()) template_dict['cleanup'] = libclinic.format_escape("".join(data.cleanup)) diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 0d83baeba9e508..26f129c0538422 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -302,6 +302,9 @@ def reset(self) -> None: self.critical_section = False self.target_critical_section = [] self.disable_fastcall = False + self.vectorcall = False + self.vectorcall_exact_only = False + self.vectorcall_zero_arg = '' self.permit_long_summary = False self.permit_long_docstring_body = False @@ -466,6 +469,24 @@ def at_staticmethod(self) -> None: fail("Can't set @staticmethod, function is not a normal callable") self.kind = STATIC_METHOD + def at_vectorcall(self, *args: str) -> None: + if self.vectorcall: + fail("Called @vectorcall twice!") + self.vectorcall = True + for arg in args: + if '=' in arg: + key, value = arg.split('=', 1) + else: + key, value = arg, '' + if key == 'exact_only': + self.vectorcall_exact_only = True + elif key == 'zero_arg': + if not value: + fail("@vectorcall zero_arg requires a value") + self.vectorcall_zero_arg = value + else: + fail(f"@vectorcall: unknown argument {key!r}") + def at_coexist(self) -> None: if self.coexist: fail("Called @coexist twice!") @@ -599,6 +620,10 @@ def normalize_function_kind(self, fullname: str) -> None: elif name == '__init__': self.kind = METHOD_INIT + # Validate @vectorcall usage. + if self.vectorcall and not self.kind.new_or_init: + fail("@vectorcall can only be used with __init__ and __new__ methods currently") + def resolve_return_converter( self, full_name: str, forced_converter: str ) -> CReturnConverter: @@ -723,7 +748,10 @@ def state_modulename_name(self, line: str) -> None: critical_section=self.critical_section, disable_fastcall=self.disable_fastcall, target_critical_section=self.target_critical_section, - forced_text_signature=self.forced_text_signature + forced_text_signature=self.forced_text_signature, + vectorcall=self.vectorcall, + vectorcall_exact_only=self.vectorcall_exact_only, + vectorcall_zero_arg=self.vectorcall_zero_arg, ) self.add_function(func) diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index f981f0bcaf89f0..303d2e0704fddb 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -111,6 +111,9 @@ class Function: critical_section: bool = False disable_fastcall: bool = False target_critical_section: list[str] = dc.field(default_factory=list) + vectorcall: bool = False + vectorcall_exact_only: bool = False + vectorcall_zero_arg: str = '' def __post_init__(self) -> None: self.parent = self.cls or self.module diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index bca87ecd75100c..b1193abb154cc6 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -5,7 +5,7 @@ from libclinic import fail, warn from libclinic.function import ( Function, Parameter, - GETTER, SETTER, METHOD_NEW) + GETTER, SETTER, METHOD_NEW, METHOD_INIT) from libclinic.converter import CConverter from libclinic.converters import ( defining_class_converter, object_converter, self_converter) @@ -650,7 +650,6 @@ def parse_var_keyword(self) -> None: self.parser_body(*parser_code) def parse_general(self, clang: CLanguage) -> None: - parsearg: str | None deprecated_positionals: dict[int, Parameter] = {} deprecated_keywords: dict[int, Parameter] = {} for i, p in enumerate(self.parameters): @@ -725,69 +724,18 @@ def parse_general(self, clang: CLanguage) -> None: fastcall=self.fastcall) parser_code.append(code) - add_label: str | None = None - for i, p in enumerate(self.parameters): - if isinstance(p.converter, defining_class_converter): - raise ValueError("defining_class should be the first " - "parameter (after clang)") - displayname = p.get_displayname(i+1) - parsearg = p.converter.parse_arg(argname_fmt % i, displayname, limited_capi=self.limited_capi) - if parsearg is None: - parser_code = [] - use_parser_code = False - break - if add_label and (i == self.pos_only or i == self.max_pos): - parser_code.append("%s:" % add_label) - add_label = None - if not p.is_optional(): - parser_code.append(libclinic.normalize_snippet(parsearg, indent=4)) - elif i < self.pos_only: - add_label = 'skip_optional_posonly' - parser_code.append(libclinic.normalize_snippet(""" - if (nargs < %d) {{ - goto %s; - }} - """ % (i + 1, add_label), indent=4)) - if has_optional_kw: - parser_code.append(libclinic.normalize_snippet(""" - noptargs--; - """, indent=4)) - parser_code.append(libclinic.normalize_snippet(parsearg, indent=4)) - else: - if i < self.max_pos: - label = 'skip_optional_pos' - first_opt = max(self.min_pos, self.pos_only) - else: - label = 'skip_optional_kwonly' - first_opt = self.max_pos + self.min_kw_only - if i == first_opt: - add_label = label - parser_code.append(libclinic.normalize_snippet(""" - if (!noptargs) {{ - goto %s; - }} - """ % add_label, indent=4)) - if i + 1 == len(self.parameters): - parser_code.append(libclinic.normalize_snippet(parsearg, indent=4)) - else: - add_label = label - parser_code.append(libclinic.normalize_snippet(""" - if (%s) {{ - """ % (argname_fmt % i), indent=4)) - parser_code.append(libclinic.normalize_snippet(parsearg, indent=8)) - parser_code.append(libclinic.normalize_snippet(""" - if (!--noptargs) {{ - goto %s; - }} - }} - """ % add_label, indent=4)) + per_arg_code, success = self._generate_keyword_per_arg_parsing( + argname_fmt=argname_fmt, + has_optional_kw=has_optional_kw, + limited_capi=self.limited_capi, + ) + if success: + parser_code.extend(per_arg_code) + else: + parser_code = [] + use_parser_code = False - if use_parser_code: - if add_label: - parser_code.append("%s:" % add_label) - if self.varpos: - parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4)) - else: + if not use_parser_code: for parameter in self.parameters: parameter.converter.use_converter() @@ -953,6 +901,7 @@ def create_template_dict(self) -> dict[str, str]: "cpp_if" : self.cpp_if, "cpp_endif" : self.cpp_endif, "methoddef_ifndef" : self.methoddef_ifndef, + "vectorcall_definition" : self.vectorcall_definition, } # make sure we didn't forget to assign something, @@ -965,6 +914,399 @@ def create_template_dict(self) -> dict[str, str]: d2[name] = value return d2 + def _vc_basename(self) -> str: + """Compute vectorcall function name from the C basename. + + Strips __init__/__new__ suffixes from c_basename and appends + _vectorcall. Respects 'as' renaming in clinic input, e.g. + 'str.__new__ as unicode_new' produces 'unicode_vectorcall'. + """ + name = self.func.c_basename + for suffix in ('___init__', '___new__', '_new', '_init'): + if name.endswith(suffix): + name = name[:-len(suffix)] + break + return f'{name}_vectorcall' + + def _generate_keyword_per_arg_parsing( + self, + *, + argname_fmt: str, + has_optional_kw: bool, + label_suffix: str = '', + limited_capi: bool = False, + ) -> tuple[list[str], bool]: + """Generate per-argument parsing code for keyword-capable functions. + + Shared between parse_general (FASTCALL|KEYWORDS) and vectorcall + keyword parsing. Returns (code_lines, success). success is False + when a converter doesn't support parse_arg. + """ + code: list[str] = [] + + def emit(text: str, indent: int = 4) -> None: + code.append(libclinic.normalize_snippet(text, indent=indent)) + + add_label: str | None = None + for i, p in enumerate(self.parameters): + if isinstance(p.converter, defining_class_converter): + raise ValueError("defining_class should be the first " + "parameter (after clang)") + displayname = p.get_displayname(i + 1) + parsearg = p.converter.parse_arg( + argname_fmt % i, displayname, limited_capi=limited_capi) + if parsearg is None: + return [], False + if add_label and (i == self.pos_only or i == self.max_pos): + code.append("%s:" % add_label) + add_label = None + if not p.is_optional(): + emit(parsearg) + elif i < self.pos_only: + add_label = f'skip_optional_posonly{label_suffix}' + emit(""" + if (nargs < %d) {{ + goto %s; + }} + """ % (i + 1, add_label)) + if has_optional_kw: + emit(""" + noptargs--; + """) + emit(parsearg) + else: + if i < self.max_pos: + label = f'skip_optional_pos{label_suffix}' + first_opt = max(self.min_pos, self.pos_only) + else: + label = f'skip_optional_kwonly{label_suffix}' + first_opt = self.max_pos + self.min_kw_only + if i == first_opt: + add_label = label + emit(""" + if (!noptargs) {{ + goto %s; + }} + """ % add_label) + if i + 1 == len(self.parameters): + emit(parsearg) + else: + add_label = label + emit(""" + if (%s) {{ + """ % (argname_fmt % i)) + emit(parsearg, indent=8) + emit(""" + if (!--noptargs) {{ + goto %s; + }} + }} + """ % add_label) + + if add_label: + code.append("%s:" % add_label) + if self.varpos: + emit(self._parse_vararg()) + return code, True + + def _generate_vc_pos_only_code( + self, + label_suffix: str = '', + indent: int = 4, + ) -> tuple[list[str], bool]: + """Generate positional-only argument parsing for vectorcall. + + Used both for the all-pos-only case and for the kwnames==NULL + fast path inside the general case. + + Returns (code_lines, success). success is False when a converter + doesn't support parse_arg (caller should fall back). + """ + max_args = NO_VARARG if self.varpos else self.max_pos + code: list[str] = [] + + def emit(text: str, ind: int = indent) -> None: + code.append(libclinic.normalize_snippet(text, indent=ind)) + + if self.min_pos or max_args != NO_VARARG: + self.codegen.add_include('pycore_modsupport.h', + '_PyArg_CheckPositional()') + emit(f""" + if (!_PyArg_CheckPositional("{{name}}", nargs, {self.min_pos}, {max_args})) {{{{ + goto exit; + }}}} + """) + + has_optional = False + skip_label = f'skip_optional_vc{label_suffix}' + for i, p in enumerate(self.parameters): + displayname = p.get_displayname(i + 1) + parsearg = p.converter.parse_arg( + f'args[{i}]', displayname, limited_capi=False) + if parsearg is None: + return [], False + if has_optional or p.is_optional(): + has_optional = True + emit(""" + if (nargs < %d) {{ + goto %s; + }} + """ % (i + 1, skip_label)) + emit(parsearg) + + if has_optional: + emit(f"{skip_label}:", ind=indent - 4) + + if self.varpos: + emit(self._parse_vararg()) + + return code, True + + def _generate_vc_parsing_code(self) -> list[str]: + """Generate FASTCALL-style argument parsing code for vectorcall.""" + no_params = (not self.parameters and not self.varpos + and not self.var_keyword) + all_pos_only = (self.pos_only == len(self.parameters) + and self.var_keyword is None) + + parser_code: list[str] = [] + snippet = libclinic.normalize_snippet + + def emit(text: str, indent: int = 4) -> None: + parser_code.append(snippet(text, indent=indent)) + + if no_params: + self.codegen.add_include('pycore_modsupport.h', + '_PyArg_NoKwnames()') + emit(""" + if (!_PyArg_NoKwnames("{name}", kwnames)) {{ + goto exit; + }} + if (nargs != 0) {{ + PyErr_Format(PyExc_TypeError, + "{name}() takes no arguments (%zd given)", + nargs); + goto exit; + }} + """) + return parser_code + elif all_pos_only: + self.codegen.add_include('pycore_modsupport.h', + '_PyArg_NoKwnames()') + emit(""" + if (!_PyArg_NoKwnames("{name}", kwnames)) {{ + goto exit; + }} + """) + + pos_code, success = self._generate_vc_pos_only_code() + if not success: + for parameter in self.parameters: + parameter.converter.use_converter() + self.codegen.add_include('pycore_modsupport.h', + '_PyArg_ParseStack()') + return [snippet(""" + if (!_PyArg_NoKwnames("{name}", kwnames)) {{ + goto exit; + }} + if (!_PyArg_ParseStack(args, nargs, "{format_units}:{name}", + {parse_arguments})) {{ + goto exit; + }} + """, indent=4)] + parser_code.extend(pos_code) + return parser_code + else: + # General case: has keyword args. Use _PyArg_UnpackKeywords + # in FASTCALL style. + + # Check if we can generate a kwnames==NULL fast path. + # This avoids the overhead of _PyArg_UnpackKeywords when + # only positional args are passed (the common case). + has_kw_only = any(p.is_keyword_only() + for p in self.parameters) + can_fast_path = (not has_kw_only and not self.varpos + and not self.var_keyword) + + if can_fast_path: + fast_code, success = self._generate_vc_pos_only_code( + label_suffix='_fast', indent=8) + if success: + emit(""" + if (kwnames == NULL) {{ + """) + parser_code.extend(fast_code) + emit(""" + goto vc_fast_end; + }} + """) + + self.codegen.add_include('pycore_modsupport.h', + '_PyArg_UnpackKeywords()') + vc_declarations = declare_parser( + self.func, codegen=self.codegen) + vc_declarations += ("\nPyObject *argsbuf[%s];" + % (len(self.converters) or 1)) + + nargs_expr = 'nargs' + if self.varpos: + nargs_expr = (f'Py_MIN(nargs, {self.max_pos})' + if self.max_pos else '0') + + has_optional_kw = ( + max(self.pos_only, self.min_pos) + self.min_kw_only + < len(self.converters) + ) + if has_optional_kw: + vc_declarations += ( + "\nPy_ssize_t noptargs = %s + " + "(kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - %d;" + % (nargs_expr, self.min_pos + self.min_kw_only)) + + emit(vc_declarations) + + emit(f""" + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, + /*minpos*/ {self.min_pos}, /*maxpos*/ {self.max_pos}, + /*minkw*/ {self.min_kw_only}, + /*varpos*/ {1 if self.varpos else 0}, argsbuf); + if (!args) {{{{ + goto exit; + }}}} + """) + + per_arg_code, success = self._generate_keyword_per_arg_parsing( + argname_fmt='args[%d]', + has_optional_kw=has_optional_kw, + label_suffix='_vc', + ) + if not success: + for parameter in self.parameters: + parameter.converter.use_converter() + self.codegen.add_include( + 'pycore_modsupport.h', + '_PyArg_ParseStackAndKeywords()') + return [ + snippet(vc_declarations, indent=4), + snippet(""" + if (!_PyArg_ParseStackAndKeywords(args, nargs, + kwnames, &_parser{parse_arguments_comma} + {parse_arguments})) {{ + goto exit; + }} + """, indent=4)] + parser_code.extend(per_arg_code) + + if can_fast_path: + parser_code.append("vc_fast_end:") + + return parser_code + + def generate_vectorcall(self) -> str: + """Generate a vectorcall function for __init__ or __new__.""" + func = self.func + vc_basename = self._vc_basename() + + # Generate argument parsing code (FASTCALL-style) + parsing_code = self._generate_vc_parsing_code() + + # Build the function prototype + prototype = libclinic.normalize_snippet(f""" + static PyObject * + {vc_basename}(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) + """) + + # Build the preamble + preamble = libclinic.normalize_snippet(""" + {{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + {declarations} + {initializers} + """) + "\n" + + # Exact type check (if vectorcall_exact_only) + exact_check = "" + if func.vectorcall_exact_only and func.cls: + type_obj = func.cls.type_object + self.codegen.add_include('pycore_call.h', + '_PyObject_MakeTpCall()') + exact_check = libclinic.normalize_snippet(f""" + if (_PyType_CAST(type) != {type_obj}) {{{{ + PyThreadState *tstate = _PyThreadState_GET(); + return _PyObject_MakeTpCall(tstate, type, args, + nargs, kwnames); + }}}} + """, indent=4) + + # Build the finale (impl call + return) + if func.kind is METHOD_INIT: + finale = libclinic.normalize_snippet(""" + {modifications} + {lock} + {{ + PyObject *self = _PyType_CAST(type)->tp_alloc( + _PyType_CAST(type), 0); + if (self == NULL) {{ + goto exit; + }} + int _result = {c_basename}_impl({vc_impl_arguments}); + {unlock} + if (_result != 0) {{ + Py_DECREF(self); + goto exit; + }} + return_value = self; + }} + {post_parsing} + + {exit_label} + {cleanup} + return return_value; + }} + """) + else: + # METHOD_NEW + finale = libclinic.normalize_snippet(""" + {modifications} + {lock} + return_value = {c_basename}_impl({vc_impl_arguments}); + {unlock} + {return_conversion} + {post_parsing} + + {exit_label} + {cleanup} + return return_value; + }} + """) + + # Zero-arg shortcut: return a cached value when called with no args + zero_arg_shortcut = "" + if func.vectorcall_zero_arg: + zero_arg_shortcut = libclinic.normalize_snippet(f""" + if (nargs == 0 && kwnames == NULL) {{{{ + return {func.vectorcall_zero_arg}; + }}}} + """, indent=4) + + # Assemble the full function + lines = [prototype] + lines.append(preamble) + if exact_check: + lines.append(exact_check) + if zero_arg_shortcut: + lines.append(zero_arg_shortcut) + lines.extend(parsing_code) + lines.append(finale) + + code = libclinic.linear_format( + "\n".join(lines), + parser_declarations='') + return code + def parse_args(self, clang: CLanguage) -> dict[str, str]: self.select_prototypes() self.init_limited_capi() @@ -975,6 +1317,7 @@ def parse_args(self, clang: CLanguage) -> dict[str, str]: self.parser_definition = "" self.impl_prototype = None self.impl_definition = IMPL_DEFINITION_PROTOTYPE + self.vectorcall_definition = "" # parser_body_fields remembers the fields passed in to the # previous call to parser_body. this is used for an awful hack. @@ -1000,4 +1343,8 @@ def parse_args(self, clang: CLanguage) -> dict[str, str]: self.process_methoddef(clang) self.finalize(clang) + # Generate vectorcall function if requested + if self.func.vectorcall: + self.vectorcall_definition = self.generate_vectorcall() + return self.create_template_dict() From a9d0d6f4186f79b50f52fa6580677e5d35a46788 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sat, 28 Feb 2026 20:36:12 -0800 Subject: [PATCH 02/20] add blurb --- .../Tools-Demos/2026-02-28-20-35-47.gh-issue-87613.Nwzu6U.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-02-28-20-35-47.gh-issue-87613.Nwzu6U.rst diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-02-28-20-35-47.gh-issue-87613.Nwzu6U.rst b/Misc/NEWS.d/next/Tools-Demos/2026-02-28-20-35-47.gh-issue-87613.Nwzu6U.rst new file mode 100644 index 00000000000000..8ff15606829437 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-02-28-20-35-47.gh-issue-87613.Nwzu6U.rst @@ -0,0 +1,3 @@ +Add a ``@vectorcall`` decorator to Argument Clinic that can be used on +``__init__`` and ``__new__`` which generates :ref:`vectorcall` argument +parsing. From 15f74e8650f26cb2898417f7be7c80d481fcf222 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sun, 1 Mar 2026 01:33:47 -0800 Subject: [PATCH 03/20] Move enumerate, reversed in enum.c to AC vectorcall --- Objects/clinic/enumobject.c.h | 94 ++++++++++++++++++++++++++++++++++- Objects/enumobject.c | 89 ++------------------------------- 2 files changed, 98 insertions(+), 85 deletions(-) diff --git a/Objects/clinic/enumobject.c.h b/Objects/clinic/enumobject.c.h index 1bda482f4955ae..9092572d1e3ca2 100644 --- a/Objects/clinic/enumobject.c.h +++ b/Objects/clinic/enumobject.c.h @@ -81,6 +81,77 @@ enum_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +enum_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *iterable; + PyObject *start = 0; + + if (kwnames == NULL) { + if (!_PyArg_CheckPositional("enumerate", nargs, 1, 2)) { + goto exit; + } + iterable = args[0]; + if (nargs < 2) { + goto skip_optional_vc_fast; + } + start = args[1]; + skip_optional_vc_fast: + goto vc_fast_end; + } + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(iterable), &_Py_ID(start), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"iterable", "start", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "enumerate", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, + /*minpos*/ 1, /*maxpos*/ 2, + /*minkw*/ 0, + /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + iterable = args[0]; + if (!noptargs) { + goto skip_optional_pos_vc; + } + start = args[1]; +skip_optional_pos_vc: +vc_fast_end: + return_value = enum_new_impl(_PyType_CAST(type), iterable, start); + +exit: + return return_value; +} + PyDoc_STRVAR(reversed_new__doc__, "reversed(object, /)\n" "--\n" @@ -110,4 +181,25 @@ reversed_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=155cc9483d5f9eab input=a9049054013a1b77]*/ + +static PyObject * +reversed_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *seq; + + if (!_PyArg_NoKwnames("reversed", kwnames)) { + goto exit; + } + if (!_PyArg_CheckPositional("reversed", nargs, 1, 1)) { + goto exit; + } + seq = args[0]; + return_value = reversed_new_impl(_PyType_CAST(type), seq); + +exit: + return return_value; +} +/*[clinic end generated code: output=d0c0441d7f42cd54 input=a9049054013a1b77]*/ diff --git a/Objects/enumobject.c b/Objects/enumobject.c index 814ce4f919514b..4c7329acba572c 100644 --- a/Objects/enumobject.c +++ b/Objects/enumobject.c @@ -28,6 +28,7 @@ typedef struct { #define _enumobject_CAST(op) ((enumobject *)(op)) /*[clinic input] +@vectorcall @classmethod enumerate.__new__ as enum_new @@ -46,7 +47,7 @@ enumerate is useful for obtaining an indexed list: static PyObject * enum_new_impl(PyTypeObject *type, PyObject *iterable, PyObject *start) -/*[clinic end generated code: output=e95e6e439f812c10 input=782e4911efcb8acf]*/ +/*[clinic end generated code: output=e95e6e439f812c10 input=a139e88889360e8f]*/ { enumobject *en; @@ -87,71 +88,6 @@ enum_new_impl(PyTypeObject *type, PyObject *iterable, PyObject *start) return (PyObject *)en; } -static int check_keyword(PyObject *kwnames, int index, - const char *name) -{ - PyObject *kw = PyTuple_GET_ITEM(kwnames, index); - if (!_PyUnicode_EqualToASCIIString(kw, name)) { - PyErr_Format(PyExc_TypeError, - "'%S' is an invalid keyword argument for enumerate()", kw); - return 0; - } - return 1; -} - -// TODO: Use AC when bpo-43447 is supported -static PyObject * -enumerate_vectorcall(PyObject *type, PyObject *const *args, - size_t nargsf, PyObject *kwnames) -{ - PyTypeObject *tp = _PyType_CAST(type); - Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); - Py_ssize_t nkwargs = 0; - if (kwnames != NULL) { - nkwargs = PyTuple_GET_SIZE(kwnames); - } - - // Manually implement enumerate(iterable, start=...) - if (nargs + nkwargs == 2) { - if (nkwargs == 1) { - if (!check_keyword(kwnames, 0, "start")) { - return NULL; - } - } else if (nkwargs == 2) { - PyObject *kw0 = PyTuple_GET_ITEM(kwnames, 0); - if (_PyUnicode_EqualToASCIIString(kw0, "start")) { - if (!check_keyword(kwnames, 1, "iterable")) { - return NULL; - } - return enum_new_impl(tp, args[1], args[0]); - } - if (!check_keyword(kwnames, 0, "iterable") || - !check_keyword(kwnames, 1, "start")) { - return NULL; - } - - } - return enum_new_impl(tp, args[0], args[1]); - } - - if (nargs + nkwargs == 1) { - if (nkwargs == 1 && !check_keyword(kwnames, 0, "iterable")) { - return NULL; - } - return enum_new_impl(tp, args[0], NULL); - } - - if (nargs == 0) { - PyErr_SetString(PyExc_TypeError, - "enumerate() missing required argument 'iterable'"); - return NULL; - } - - PyErr_Format(PyExc_TypeError, - "enumerate() takes at most 2 arguments (%d given)", nargs + nkwargs); - return NULL; -} - static void enum_dealloc(PyObject *op) { @@ -350,7 +286,7 @@ PyTypeObject PyEnum_Type = { PyType_GenericAlloc, /* tp_alloc */ enum_new, /* tp_new */ PyObject_GC_Del, /* tp_free */ - .tp_vectorcall = enumerate_vectorcall + .tp_vectorcall = enum_vectorcall }; /* Reversed Object ***************************************************************/ @@ -364,6 +300,7 @@ typedef struct { #define _reversedobject_CAST(op) ((reversedobject *)(op)) /*[clinic input] +@vectorcall @classmethod reversed.__new__ as reversed_new @@ -375,7 +312,7 @@ Return a reverse iterator over the values of the given sequence. static PyObject * reversed_new_impl(PyTypeObject *type, PyObject *seq) -/*[clinic end generated code: output=f7854cc1df26f570 input=4781869729e3ba50]*/ +/*[clinic end generated code: output=f7854cc1df26f570 input=7db568182ab28c59]*/ { Py_ssize_t n; PyObject *reversed_meth; @@ -417,22 +354,6 @@ reversed_new_impl(PyTypeObject *type, PyObject *seq) return (PyObject *)ro; } -static PyObject * -reversed_vectorcall(PyObject *type, PyObject * const*args, - size_t nargsf, PyObject *kwnames) -{ - if (!_PyArg_NoKwnames("reversed", kwnames)) { - return NULL; - } - - Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); - if (!_PyArg_CheckPositional("reversed", nargs, 1, 1)) { - return NULL; - } - - return reversed_new_impl(_PyType_CAST(type), args[0]); -} - static void reversed_dealloc(PyObject *op) { From a243517bee269deff44d479d92a444b886b2594e Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sun, 1 Mar 2026 01:35:42 -0800 Subject: [PATCH 04/20] Move tuple to AC vectorcall --- Objects/clinic/tupleobject.c.h | 30 +++++++++++++++++++++++++++++- Objects/tupleobject.c | 24 ++---------------------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/Objects/clinic/tupleobject.c.h b/Objects/clinic/tupleobject.c.h index 1c12706c0bb43b..937d1764e2d610 100644 --- a/Objects/clinic/tupleobject.c.h +++ b/Objects/clinic/tupleobject.c.h @@ -111,6 +111,34 @@ tuple_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +tuple_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *iterable = NULL; + + if (nargs == 0 && kwnames == NULL) { + return (PyObject*)&_Py_SINGLETON(tuple_empty); + } + if (!_PyArg_NoKwnames("tuple", kwnames)) { + goto exit; + } + if (!_PyArg_CheckPositional("tuple", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional_vc; + } + iterable = args[0]; +skip_optional_vc: + return_value = tuple_new_impl(_PyType_CAST(type), iterable); + +exit: + return return_value; +} + PyDoc_STRVAR(tuple___getnewargs____doc__, "__getnewargs__($self, /)\n" "--\n" @@ -127,4 +155,4 @@ tuple___getnewargs__(PyObject *self, PyObject *Py_UNUSED(ignored)) { return tuple___getnewargs___impl((PyTupleObject *)self); } -/*[clinic end generated code: output=bd11662d62d973c2 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c1e02d9c2b36d1df input=a9049054013a1b77]*/ diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 169ac69701da11..7de7ae20d1ae8c 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -713,6 +713,7 @@ static PyObject * tuple_subtype_new(PyTypeObject *type, PyObject *iterable); /*[clinic input] +@vectorcall zero_arg=(PyObject*)&_Py_SINGLETON(tuple_empty) @classmethod tuple.__new__ as tuple_new iterable: object(c_default="NULL") = () @@ -728,7 +729,7 @@ If the argument is a tuple, the return value is the same object. static PyObject * tuple_new_impl(PyTypeObject *type, PyObject *iterable) -/*[clinic end generated code: output=4546d9f0d469bce7 input=86963bcde633b5a2]*/ +/*[clinic end generated code: output=4546d9f0d469bce7 input=fff66d7a13734d92]*/ { if (type != &PyTuple_Type) return tuple_subtype_new(type, iterable); @@ -741,27 +742,6 @@ tuple_new_impl(PyTypeObject *type, PyObject *iterable) } } -static PyObject * -tuple_vectorcall(PyObject *type, PyObject * const*args, - size_t nargsf, PyObject *kwnames) -{ - if (!_PyArg_NoKwnames("tuple", kwnames)) { - return NULL; - } - - Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); - if (!_PyArg_CheckPositional("tuple", nargs, 0, 1)) { - return NULL; - } - - if (nargs) { - return tuple_new_impl(_PyType_CAST(type), args[0]); - } - else { - return tuple_get_empty(); - } -} - static PyObject * tuple_subtype_new(PyTypeObject *type, PyObject *iterable) { From 3698a32d11bc83e86a4c82c9e389ee28cd723eae Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 17 Mar 2026 22:51:06 -0700 Subject: [PATCH 05/20] Add _testclinic vectorcall sample + testing --- Lib/test/test_clinic.py | 83 +++++ Modules/_testclinic.c | 146 ++++++++ Modules/clinic/_testclinic.c.h | 531 +++++++++++++++++++++++++++- Modules/clinic/_testclinic_depr.c.h | 3 +- Modules/clinic/_testclinic_kwds.c.h | 3 +- 5 files changed, 763 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index def49c970e5467..8e6d471b8544ad 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -4293,6 +4293,89 @@ def test_kwds_with_pos_only_and_stararg(self): self.assertEqual(ac_tester.kwds_with_pos_only_and_stararg(1, 2, *args, **kwds), (1, 2, args, kwds)) +@unittest.skipIf(ac_tester is None, "_testclinic is missing") +class VectorcallFunctionalTest(unittest.TestCase): + """Runtime tests for @vectorcall exemplar types.""" + + def test_vc_new_no_args(self): + obj = ac_tester.VcNew() + self.assertIsInstance(obj, ac_tester.VcNew) + + def test_vc_new_with_arg(self): + obj = ac_tester.VcNew(1) + self.assertIsInstance(obj, ac_tester.VcNew) + + def test_vc_new_with_kwarg(self): + obj = ac_tester.VcNew(a=1) + self.assertIsInstance(obj, ac_tester.VcNew) + + def test_vc_new_rejects_extra_args(self): + with self.assertRaises(TypeError): + ac_tester.VcNew(1, 2) + + def test_vc_init_required_pos_only(self): + obj = ac_tester.VcInit(1) + self.assertIsInstance(obj, ac_tester.VcInit) + + def test_vc_init_with_keyword(self): + obj = ac_tester.VcInit(1, b=2) + self.assertIsInstance(obj, ac_tester.VcInit) + + def test_vc_init_with_positional_optional(self): + obj = ac_tester.VcInit(1, 2) + self.assertIsInstance(obj, ac_tester.VcInit) + + def test_vc_init_missing_required(self): + with self.assertRaises(TypeError): + ac_tester.VcInit() + + def test_vc_init_rejects_a_as_keyword(self): + # 'a' is positional-only + with self.assertRaises(TypeError): + ac_tester.VcInit(a=1) + + def test_vc_new_exact_one_arg(self): + obj = ac_tester.VcNewExact(1) + self.assertIsInstance(obj, ac_tester.VcNewExact) + + def test_vc_new_exact_two_args(self): + obj = ac_tester.VcNewExact(1, 2) + self.assertIsInstance(obj, ac_tester.VcNewExact) + + def test_vc_new_exact_missing_required(self): + with self.assertRaises(TypeError): + ac_tester.VcNewExact() + + def test_vc_new_exact_subclass(self): + # exact_only: subclass goes through non-vectorcall (tp_new) path + Sub = type('Sub', (ac_tester.VcNewExact,), {}) + obj = Sub(1) + self.assertIsInstance(obj, Sub) + self.assertIsInstance(obj, ac_tester.VcNewExact) + + def test_vc_new_zeroarg_no_args(self): + # zero_arg returns Py_None when called with no arguments + result = ac_tester.VcNewZeroArg() + self.assertIs(result, None) + + def test_vc_new_zeroarg_with_pos(self): + obj = ac_tester.VcNewZeroArg(1) + self.assertIsInstance(obj, ac_tester.VcNewZeroArg) + + def test_vc_new_zeroarg_with_kwonly(self): + obj = ac_tester.VcNewZeroArg(b=2) + self.assertIsInstance(obj, ac_tester.VcNewZeroArg) + + def test_vc_new_zeroarg_with_both(self): + obj = ac_tester.VcNewZeroArg(1, b=2) + self.assertIsInstance(obj, ac_tester.VcNewZeroArg) + + def test_vc_new_zeroarg_rejects_a_as_keyword(self): + # 'a' is positional-only + with self.assertRaises(TypeError): + ac_tester.VcNewZeroArg(a=1) + + class LimitedCAPIOutputTests(unittest.TestCase): def setUp(self): diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index 890f2201b46bc0..7852550ea13be7 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -21,6 +21,13 @@ custom_converter(PyObject *obj, custom_t *val) } +/* Forward declarations for vectorcall exemplar types, needed because + * clinic/_testclinic.c.h is included before the type definitions. */ +static PyTypeObject VcNew_Type; +static PyTypeObject VcInit_Type; +static PyTypeObject VcNewExact_Type; +static PyTypeObject VcNewZeroArg_Type; + #include "clinic/_testclinic.c.h" @@ -2314,6 +2321,133 @@ output pop /*[clinic end generated code: output=da39a3ee5e6b4b0d input=e7c7c42daced52b0]*/ +/* + * Vectorcall exemplars. + * + * Each type exercises a different @vectorcall code-generation path. + * There is one type per exemplar because tp_vectorcall is a single slot. + */ + + +/* --- VcNew: @vectorcall on __new__, single pos-or-kw optional + * (general + kw fast-path, METHOD_NEW finale) --- */ + +/*[clinic input] +class _testclinic.VcNew "PyObject *" "&VcNew_Type" +@classmethod +@vectorcall +_testclinic.VcNew.__new__ as vc_plain_new + a: object = None +[clinic start generated code]*/ + +static PyObject * +vc_plain_new_impl(PyTypeObject *type, PyObject *a) +/*[clinic end generated code: output=55b273e9797a3013 input=e15d88606280badc]*/ +{ + return type->tp_alloc(type, 0); +} + +static PyTypeObject VcNew_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_testclinic.VcNew", + .tp_basicsize = sizeof(PyObject), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = vc_plain_new, + .tp_vectorcall = vc_plain_vectorcall, +}; + + +/* --- VcInit: @vectorcall on __init__, pos-only + pos-or-kw optional + * (general + kw fast-path) --- */ + +/*[clinic input] +class _testclinic.VcInit "PyObject *" "&VcInit_Type" +@vectorcall +_testclinic.VcInit.__init__ as vc_posorkw_init + a: object + / + b: object = None +[clinic start generated code]*/ + +static int +vc_posorkw_init_impl(PyObject *self, PyObject *a, PyObject *b) +/*[clinic end generated code: output=6018424ba9fb0744 input=25e4c2b792040c31]*/ +{ + return 0; +} + +static PyTypeObject VcInit_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_testclinic.VcInit", + .tp_basicsize = sizeof(PyObject), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = vc_posorkw_init, + .tp_vectorcall = vc_posorkw_vectorcall, +}; + + +/* --- VcNewExact: @vectorcall exact_only on __new__, pos-only required + + * pos-or-kw optional (general + kw fast-path + exact_only guard) --- */ + +/*[clinic input] +class _testclinic.VcNewExact "PyObject *" "&VcNewExact_Type" +@classmethod +@vectorcall exact_only +_testclinic.VcNewExact.__new__ as vc_exact_new + a: object + / + b: object = None +[clinic start generated code]*/ + +static PyObject * +vc_exact_new_impl(PyTypeObject *type, PyObject *a, PyObject *b) +/*[clinic end generated code: output=e88217e36443b698 input=ea86a1ab634c93a6]*/ +{ + return type->tp_alloc(type, 0); +} + +static PyTypeObject VcNewExact_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_testclinic.VcNewExact", + .tp_basicsize = sizeof(PyObject), + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = vc_exact_new, + .tp_vectorcall = vc_exact_vectorcall, +}; + + +/* --- VcNewZeroArg: @vectorcall zero_arg on __new__, pos-only optional + + * kw-only optional (general path, no kw fast-path + zero_arg) --- */ + +/*[clinic input] +class _testclinic.VcNewZeroArg "PyObject *" "&VcNewZeroArg_Type" +@classmethod +@vectorcall zero_arg=Py_NewRef(Py_None) +_testclinic.VcNewZeroArg.__new__ as vc_zeroarg_new + a: object = None + / + * + b: object = None +[clinic start generated code]*/ + +static PyObject * +vc_zeroarg_new_impl(PyTypeObject *type, PyObject *a, PyObject *b) +/*[clinic end generated code: output=6425b64d61c6317a input=f3d3ba860fc40034]*/ +{ + return type->tp_alloc(type, 0); +} + +static PyTypeObject VcNewZeroArg_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_testclinic.VcNewZeroArg", + .tp_basicsize = sizeof(PyObject), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = vc_zeroarg_new, + .tp_vectorcall = vc_zeroarg_vectorcall, +}; + + /*[clinic input] output push destination kwarg new file '{dirname}/clinic/_testclinic_kwds.c.h' @@ -2533,6 +2667,18 @@ PyInit__testclinic(void) if (PyModule_AddType(m, &DeprKwdInitNoInline) < 0) { goto error; } + if (PyModule_AddType(m, &VcNew_Type) < 0) { + goto error; + } + if (PyModule_AddType(m, &VcInit_Type) < 0) { + goto error; + } + if (PyModule_AddType(m, &VcNewExact_Type) < 0) { + goto error; + } + if (PyModule_AddType(m, &VcNewZeroArg_Type) < 0) { + goto error; + } return m; error: diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index 9bcd0eeb008142..cd6c4651ff435b 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -6,6 +6,7 @@ preserve # include "pycore_gc.h" // PyGC_Head #endif #include "pycore_abstract.h" // _PyNumber_Index() +#include "pycore_call.h" // _PyObject_MakeTpCall() #include "pycore_long.h" // _PyLong_UnsignedShort_Converter() #include "pycore_modsupport.h" // _PyArg_CheckPositional() #include "pycore_runtime.h" // _Py_ID() @@ -4600,4 +4601,532 @@ _testclinic_TestClass_posonly_poskw_varpos_array_no_fastcall(PyObject *type, PyO exit: return return_value; } -/*[clinic end generated code: output=290d2e346ea7bfa1 input=a9049054013a1b77]*/ + +static PyObject * +vc_plain_new_impl(PyTypeObject *type, PyObject *a); + +static PyObject * +vc_plain_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('a'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"a", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcNew", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + PyObject *a = Py_None; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + a = fastargs[0]; +skip_optional_pos: + return_value = vc_plain_new_impl(type, a); + +exit: + return return_value; +} + +static PyObject * +vc_plain_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *a = Py_None; + + if (kwnames == NULL) { + if (!_PyArg_CheckPositional("VcNew", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional_vc_fast; + } + a = args[0]; + skip_optional_vc_fast: + goto vc_fast_end; + } + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('a'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"a", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcNew", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, + /*minpos*/ 0, /*maxpos*/ 1, + /*minkw*/ 0, + /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos_vc; + } + a = args[0]; +skip_optional_pos_vc: +vc_fast_end: + return_value = vc_plain_new_impl(_PyType_CAST(type), a); + +exit: + return return_value; +} + +static int +vc_posorkw_init_impl(PyObject *self, PyObject *a, PyObject *b); + +static int +vc_posorkw_init(PyObject *self, PyObject *args, PyObject *kwargs) +{ + int return_value = -1; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('b'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "b", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcInit", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1; + PyObject *a; + PyObject *b = Py_None; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + a = fastargs[0]; + if (!noptargs) { + goto skip_optional_pos; + } + b = fastargs[1]; +skip_optional_pos: + return_value = vc_posorkw_init_impl(self, a, b); + +exit: + return return_value; +} + +static PyObject * +vc_posorkw_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *a; + PyObject *b = Py_None; + + if (kwnames == NULL) { + if (!_PyArg_CheckPositional("VcInit", nargs, 1, 2)) { + goto exit; + } + a = args[0]; + if (nargs < 2) { + goto skip_optional_vc_fast; + } + b = args[1]; + skip_optional_vc_fast: + goto vc_fast_end; + } + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('b'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "b", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcInit", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, + /*minpos*/ 1, /*maxpos*/ 2, + /*minkw*/ 0, + /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + a = args[0]; + if (!noptargs) { + goto skip_optional_pos_vc; + } + b = args[1]; +skip_optional_pos_vc: +vc_fast_end: + { + PyObject *self = _PyType_CAST(type)->tp_alloc( + _PyType_CAST(type), 0); + if (self == NULL) { + goto exit; + } + int _result = vc_posorkw_init_impl((PyObject *)self, a, b); + if (_result != 0) { + Py_DECREF(self); + goto exit; + } + return_value = self; + } + +exit: + return return_value; +} + +static PyObject * +vc_exact_new_impl(PyTypeObject *type, PyObject *a, PyObject *b); + +static PyObject * +vc_exact_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('b'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "b", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcNewExact", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1; + PyObject *a; + PyObject *b = Py_None; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + a = fastargs[0]; + if (!noptargs) { + goto skip_optional_pos; + } + b = fastargs[1]; +skip_optional_pos: + return_value = vc_exact_new_impl(type, a, b); + +exit: + return return_value; +} + +static PyObject * +vc_exact_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *a; + PyObject *b = Py_None; + + if (_PyType_CAST(type) != &VcNewExact_Type) { + PyThreadState *tstate = _PyThreadState_GET(); + return _PyObject_MakeTpCall(tstate, type, args, + nargs, kwnames); + } + if (kwnames == NULL) { + if (!_PyArg_CheckPositional("VcNewExact", nargs, 1, 2)) { + goto exit; + } + a = args[0]; + if (nargs < 2) { + goto skip_optional_vc_fast; + } + b = args[1]; + skip_optional_vc_fast: + goto vc_fast_end; + } + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('b'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "b", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcNewExact", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, + /*minpos*/ 1, /*maxpos*/ 2, + /*minkw*/ 0, + /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + a = args[0]; + if (!noptargs) { + goto skip_optional_pos_vc; + } + b = args[1]; +skip_optional_pos_vc: +vc_fast_end: + return_value = vc_exact_new_impl(_PyType_CAST(type), a, b); + +exit: + return return_value; +} + +static PyObject * +vc_zeroarg_new_impl(PyTypeObject *type, PyObject *a, PyObject *b); + +static PyObject * +vc_zeroarg_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('b'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "b", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcNewZeroArg", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + PyObject *a = Py_None; + PyObject *b = Py_None; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + if (nargs < 1) { + goto skip_optional_posonly; + } + noptargs--; + a = fastargs[0]; +skip_optional_posonly: + if (!noptargs) { + goto skip_optional_kwonly; + } + b = fastargs[1]; +skip_optional_kwonly: + return_value = vc_zeroarg_new_impl(type, a, b); + +exit: + return return_value; +} + +static PyObject * +vc_zeroarg_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *a = Py_None; + PyObject *b = Py_None; + + if (nargs == 0 && kwnames == NULL) { + return Py_NewRef(Py_None); + } + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('b'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "b", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcNewZeroArg", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, + /*minpos*/ 0, /*maxpos*/ 1, + /*minkw*/ 0, + /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + if (nargs < 1) { + goto skip_optional_posonly_vc; + } + noptargs--; + a = args[0]; +skip_optional_posonly_vc: + if (!noptargs) { + goto skip_optional_kwonly_vc; + } + b = args[1]; +skip_optional_kwonly_vc: + return_value = vc_zeroarg_new_impl(_PyType_CAST(type), a, b); + +exit: + return return_value; +} +/*[clinic end generated code: output=59b2d8034cc0b97f input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_testclinic_depr.c.h b/Modules/clinic/_testclinic_depr.c.h index e2db4fd87ed26b..4434bb71207c19 100644 --- a/Modules/clinic/_testclinic_depr.c.h +++ b/Modules/clinic/_testclinic_depr.c.h @@ -6,6 +6,7 @@ preserve # include "pycore_gc.h" // PyGC_Head #endif #include "pycore_abstract.h" // _PyNumber_Index() +#include "pycore_call.h" // _PyObject_MakeTpCall() #include "pycore_long.h" // _PyLong_UnsignedShort_Converter() #include "pycore_modsupport.h" // _PyArg_CheckPositional() #include "pycore_runtime.h" // _Py_ID() @@ -2474,4 +2475,4 @@ depr_multi(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject * exit: return return_value; } -/*[clinic end generated code: output=2231bec0ed196830 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=068be10cbdafd91e input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_testclinic_kwds.c.h b/Modules/clinic/_testclinic_kwds.c.h index 86cad50c56cf55..1f92727249f950 100644 --- a/Modules/clinic/_testclinic_kwds.c.h +++ b/Modules/clinic/_testclinic_kwds.c.h @@ -6,6 +6,7 @@ preserve # include "pycore_gc.h" // PyGC_Head #endif #include "pycore_abstract.h" // _PyNumber_Index() +#include "pycore_call.h" // _PyObject_MakeTpCall() #include "pycore_long.h" // _PyLong_UnsignedShort_Converter() #include "pycore_modsupport.h" // _PyArg_CheckPositional() #include "pycore_runtime.h" // _Py_ID() @@ -181,4 +182,4 @@ kwds_with_pos_only_and_stararg(PyObject *module, PyObject *args, PyObject *kwarg return return_value; } -/*[clinic end generated code: output=3e5251b10aa44382 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=937ae7c4a6176de1 input=a9049054013a1b77]*/ From 04a148c81460220ceaad9a71d66e186c4dcb311f Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 18 May 2026 15:24:08 -0700 Subject: [PATCH 06/20] Simplify some test cases --- Lib/test/test_clinic.py | 96 ++++++---------------------------- Modules/_testclinic.c | 20 ++----- Modules/clinic/_testclinic.c.h | 3 +- 3 files changed, 21 insertions(+), 98 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 36cc755fd7435c..af486eb720852a 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -2751,47 +2751,6 @@ class Foo "FooObject *" "Foo_Type" self.assertTrue(func.vectorcall) self.assertTrue(func.vectorcall_exact_only) - def test_vectorcall_init_with_kwargs(self): - block = """ - module m - class Foo "FooObject *" "Foo_Type" - @vectorcall - Foo.__init__ - source: object = NULL - encoding: str = NULL - errors: str = NULL - """ - func = self.parse_function(block, signatures_in_block=3, - function_index=2) - self.assertTrue(func.vectorcall) - - def test_vectorcall_new_with_kwargs(self): - block = """ - module m - class Foo "FooObject *" "Foo_Type" - @classmethod - @vectorcall - Foo.__new__ - source: object = NULL - * - encoding: str = NULL - errors: str = NULL - """ - func = self.parse_function(block, signatures_in_block=3, - function_index=2) - self.assertTrue(func.vectorcall) - - def test_vectorcall_init_no_args(self): - block = """ - module m - class Foo "FooObject *" "Foo_Type" - @vectorcall - Foo.__init__ - """ - func = self.parse_function(block, signatures_in_block=3, - function_index=2) - self.assertTrue(func.vectorcall) - def test_vectorcall_zero_arg(self): block = """ module m @@ -4478,33 +4437,19 @@ def test_kwds_with_pos_only_and_stararg(self): class VectorcallFunctionalTest(unittest.TestCase): """Runtime tests for @vectorcall exemplar types.""" - def test_vc_new_no_args(self): - obj = ac_tester.VcNew() - self.assertIsInstance(obj, ac_tester.VcNew) - - def test_vc_new_with_arg(self): - obj = ac_tester.VcNew(1) - self.assertIsInstance(obj, ac_tester.VcNew) - - def test_vc_new_with_kwarg(self): - obj = ac_tester.VcNew(a=1) - self.assertIsInstance(obj, ac_tester.VcNew) + def test_vc_new(self): + self.assertIsInstance(ac_tester.VcNew(), ac_tester.VcNew) + self.assertIsInstance(ac_tester.VcNew(1), ac_tester.VcNew) + self.assertIsInstance(ac_tester.VcNew(a=1), ac_tester.VcNew) def test_vc_new_rejects_extra_args(self): with self.assertRaises(TypeError): ac_tester.VcNew(1, 2) - def test_vc_init_required_pos_only(self): - obj = ac_tester.VcInit(1) - self.assertIsInstance(obj, ac_tester.VcInit) - - def test_vc_init_with_keyword(self): - obj = ac_tester.VcInit(1, b=2) - self.assertIsInstance(obj, ac_tester.VcInit) - - def test_vc_init_with_positional_optional(self): - obj = ac_tester.VcInit(1, 2) - self.assertIsInstance(obj, ac_tester.VcInit) + def test_vc_init(self): + self.assertIsInstance(ac_tester.VcInit(1), ac_tester.VcInit) + self.assertIsInstance(ac_tester.VcInit(1, 2), ac_tester.VcInit) + self.assertIsInstance(ac_tester.VcInit(1, b=2), ac_tester.VcInit) def test_vc_init_missing_required(self): with self.assertRaises(TypeError): @@ -4515,13 +4460,9 @@ def test_vc_init_rejects_a_as_keyword(self): with self.assertRaises(TypeError): ac_tester.VcInit(a=1) - def test_vc_new_exact_one_arg(self): - obj = ac_tester.VcNewExact(1) - self.assertIsInstance(obj, ac_tester.VcNewExact) - - def test_vc_new_exact_two_args(self): - obj = ac_tester.VcNewExact(1, 2) - self.assertIsInstance(obj, ac_tester.VcNewExact) + def test_vc_new_exact(self): + self.assertIsInstance(ac_tester.VcNewExact(1), ac_tester.VcNewExact) + self.assertIsInstance(ac_tester.VcNewExact(1, 2), ac_tester.VcNewExact) def test_vc_new_exact_missing_required(self): with self.assertRaises(TypeError): @@ -4539,17 +4480,10 @@ def test_vc_new_zeroarg_no_args(self): result = ac_tester.VcNewZeroArg() self.assertIs(result, None) - def test_vc_new_zeroarg_with_pos(self): - obj = ac_tester.VcNewZeroArg(1) - self.assertIsInstance(obj, ac_tester.VcNewZeroArg) - - def test_vc_new_zeroarg_with_kwonly(self): - obj = ac_tester.VcNewZeroArg(b=2) - self.assertIsInstance(obj, ac_tester.VcNewZeroArg) - - def test_vc_new_zeroarg_with_both(self): - obj = ac_tester.VcNewZeroArg(1, b=2) - self.assertIsInstance(obj, ac_tester.VcNewZeroArg) + def test_vc_new_zeroarg_with_args(self): + self.assertIsInstance(ac_tester.VcNewZeroArg(1), ac_tester.VcNewZeroArg) + self.assertIsInstance(ac_tester.VcNewZeroArg(b=2), ac_tester.VcNewZeroArg) + self.assertIsInstance(ac_tester.VcNewZeroArg(1, b=2), ac_tester.VcNewZeroArg) def test_vc_new_zeroarg_rejects_a_as_keyword(self): # 'a' is positional-only diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index 7501c82237e840..d08f05f3e9441c 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -2322,16 +2322,9 @@ output pop /*[clinic end generated code: output=da39a3ee5e6b4b0d input=e7c7c42daced52b0]*/ -/* - * Vectorcall exemplars. - * - * Each type exercises a different @vectorcall code-generation path. - * There is one type per exemplar because tp_vectorcall is a single slot. - */ - +/* @vectorcall test types. One type per exemplar because tp_vectorcall is a single slot. */ -/* --- VcNew: @vectorcall on __new__, single pos-or-kw optional - * (general + kw fast-path, METHOD_NEW finale) --- */ +/* VcNew: __new__ with one optional positional-or-keyword arg */ /*[clinic input] class _testclinic.VcNew "PyObject *" "&VcNew_Type" @@ -2358,8 +2351,7 @@ static PyTypeObject VcNew_Type = { }; -/* --- VcInit: @vectorcall on __init__, pos-only + pos-or-kw optional - * (general + kw fast-path) --- */ +/* VcInit: __init__ with one required positional-only and one optional keyword arg */ /*[clinic input] class _testclinic.VcInit "PyObject *" "&VcInit_Type" @@ -2388,8 +2380,7 @@ static PyTypeObject VcInit_Type = { }; -/* --- VcNewExact: @vectorcall exact_only on __new__, pos-only required + - * pos-or-kw optional (general + kw fast-path + exact_only guard) --- */ +/* VcNewExact: __new__ with exact_only; subclasses fall back to tp_new */ /*[clinic input] class _testclinic.VcNewExact "PyObject *" "&VcNewExact_Type" @@ -2418,8 +2409,7 @@ static PyTypeObject VcNewExact_Type = { }; -/* --- VcNewZeroArg: @vectorcall zero_arg on __new__, pos-only optional + - * kw-only optional (general path, no kw fast-path + zero_arg) --- */ +/* VcNewZeroArg: __new__ with zero_arg; returns Py_None when called with no args */ /*[clinic input] class _testclinic.VcNewZeroArg "PyObject *" "&VcNewZeroArg_Type" diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index f41df5b830250c..29ab6097c64902 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -4602,7 +4602,6 @@ _testclinic_TestClass_posonly_poskw_varpos_array_no_fastcall(PyObject *type, PyO return return_value; } - static PyObject * vc_plain_new_impl(PyTypeObject *type, PyObject *a); @@ -5130,4 +5129,4 @@ vc_zeroarg_vectorcall(PyObject *type, PyObject *const *args, exit: return return_value; } -/*[clinic end generated code: output=59b2d8034cc0b97f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a8d5b01f87544445 input=a9049054013a1b77]*/ From b5d2e40632b62b184fe281fb9390c309a76a4502 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 18 May 2026 17:08:00 -0700 Subject: [PATCH 07/20] Ignore very large _testclinic --- Tools/c-analyzer/cpython/_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index d7248c34c59be4..e47b1a2fd514b9 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -343,7 +343,7 @@ def format_tsv_lines(lines): _abs('Modules/_ssl_data_300.h'): (80_000, 10_000), _abs('Modules/_ssl_data_111.h'): (80_000, 10_000), _abs('Modules/cjkcodecs/mappings_*.h'): (160_000, 2_000), - _abs('Modules/clinic/_testclinic.c.h'): (125_000, 5_000), + _abs('Modules/clinic/_testclinic.c.h'): (130_000, 5_000), _abs('Modules/unicodedata_db.h'): (180_000, 3_000), _abs('Modules/unicodename_db.h'): (1_200_000, 15_000), _abs('Objects/unicodetype_db.h'): (240_000, 3_000), From 71b0d8e1d7e30c463c002493dc122ed244ac70a1 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 18 May 2026 17:17:28 -0700 Subject: [PATCH 08/20] Up limit a bit more --- Tools/c-analyzer/cpython/_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index e47b1a2fd514b9..f5438ee4908bc1 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -343,7 +343,7 @@ def format_tsv_lines(lines): _abs('Modules/_ssl_data_300.h'): (80_000, 10_000), _abs('Modules/_ssl_data_111.h'): (80_000, 10_000), _abs('Modules/cjkcodecs/mappings_*.h'): (160_000, 2_000), - _abs('Modules/clinic/_testclinic.c.h'): (130_000, 5_000), + _abs('Modules/clinic/_testclinic.c.h'): (180_000, 5_000), _abs('Modules/unicodedata_db.h'): (180_000, 3_000), _abs('Modules/unicodename_db.h'): (1_200_000, 15_000), _abs('Objects/unicodetype_db.h'): (240_000, 3_000), From b96c32625ca402fc5437cc17eff22d29c88f8635 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 18 May 2026 21:28:59 -0700 Subject: [PATCH 09/20] Exclude more _testclinic from globals --- Tools/c-analyzer/cpython/globals-to-fix.tsv | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index db575d870be5c5..7979fcefdfc092 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -353,6 +353,10 @@ Modules/_testclinic.c - DeprKwdInit - Modules/_testclinic.c - DeprKwdInitNoInline - Modules/_testclinic.c - DeprKwdNew - Modules/_testclinic.c - TestClass - +Modules/_testclinic.c - VcInit_Type - +Modules/_testclinic.c - VcNew_Type - +Modules/_testclinic.c - VcNewExact_Type - +Modules/_testclinic.c - VcNewZeroArg_Type - ################################## From 379fd85db6a2a196f6d19e31e060452b16b986d4 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 19 May 2026 14:20:58 -0700 Subject: [PATCH 10/20] Remove zero_arg case --- Lib/test/test_clinic.py | 47 ---------- Modules/_testclinic.c | 34 ------- Modules/clinic/_testclinic.c.h | 131 +-------------------------- Objects/clinic/tupleobject.c.h | 5 +- Objects/tupleobject.c | 4 +- Tools/clinic/libclinic/dsl_parser.py | 6 -- Tools/clinic/libclinic/function.py | 1 - Tools/clinic/libclinic/parse_args.py | 11 --- 8 files changed, 4 insertions(+), 235 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index af486eb720852a..2e0da60add4c34 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -2751,38 +2751,6 @@ class Foo "FooObject *" "Foo_Type" self.assertTrue(func.vectorcall) self.assertTrue(func.vectorcall_exact_only) - def test_vectorcall_zero_arg(self): - block = """ - module m - class Foo "FooObject *" "Foo_Type" - @classmethod - @vectorcall zero_arg=_PyFoo_GetEmpty() - Foo.__new__ - x: object = NULL - / - """ - func = self.parse_function(block, signatures_in_block=3, - function_index=2) - self.assertTrue(func.vectorcall) - self.assertFalse(func.vectorcall_exact_only) - self.assertEqual(func.vectorcall_zero_arg, '_PyFoo_GetEmpty()') - - def test_vectorcall_zero_arg_with_exact(self): - block = """ - module m - class Foo "FooObject *" "Foo_Type" - @classmethod - @vectorcall exact_only zero_arg=get_cached() - Foo.__new__ - x: object = NULL - / - """ - func = self.parse_function(block, signatures_in_block=3, - function_index=2) - self.assertTrue(func.vectorcall) - self.assertTrue(func.vectorcall_exact_only) - self.assertEqual(func.vectorcall_zero_arg, 'get_cached()') - def test_vectorcall_invalid_kwarg(self): err = "unknown argument" block = """ @@ -4475,21 +4443,6 @@ def test_vc_new_exact_subclass(self): self.assertIsInstance(obj, Sub) self.assertIsInstance(obj, ac_tester.VcNewExact) - def test_vc_new_zeroarg_no_args(self): - # zero_arg returns Py_None when called with no arguments - result = ac_tester.VcNewZeroArg() - self.assertIs(result, None) - - def test_vc_new_zeroarg_with_args(self): - self.assertIsInstance(ac_tester.VcNewZeroArg(1), ac_tester.VcNewZeroArg) - self.assertIsInstance(ac_tester.VcNewZeroArg(b=2), ac_tester.VcNewZeroArg) - self.assertIsInstance(ac_tester.VcNewZeroArg(1, b=2), ac_tester.VcNewZeroArg) - - def test_vc_new_zeroarg_rejects_a_as_keyword(self): - # 'a' is positional-only - with self.assertRaises(TypeError): - ac_tester.VcNewZeroArg(a=1) - class LimitedCAPIOutputTests(unittest.TestCase): diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index d08f05f3e9441c..a35d7447486045 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -26,8 +26,6 @@ custom_converter(PyObject *obj, custom_t *val) static PyTypeObject VcNew_Type; static PyTypeObject VcInit_Type; static PyTypeObject VcNewExact_Type; -static PyTypeObject VcNewZeroArg_Type; - #include "clinic/_testclinic.c.h" @@ -2409,35 +2407,6 @@ static PyTypeObject VcNewExact_Type = { }; -/* VcNewZeroArg: __new__ with zero_arg; returns Py_None when called with no args */ - -/*[clinic input] -class _testclinic.VcNewZeroArg "PyObject *" "&VcNewZeroArg_Type" -@classmethod -@vectorcall zero_arg=Py_NewRef(Py_None) -_testclinic.VcNewZeroArg.__new__ as vc_zeroarg_new - a: object = None - / - * - b: object = None -[clinic start generated code]*/ - -static PyObject * -vc_zeroarg_new_impl(PyTypeObject *type, PyObject *a, PyObject *b) -/*[clinic end generated code: output=6425b64d61c6317a input=f3d3ba860fc40034]*/ -{ - return type->tp_alloc(type, 0); -} - -static PyTypeObject VcNewZeroArg_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "_testclinic.VcNewZeroArg", - .tp_basicsize = sizeof(PyObject), - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_new = vc_zeroarg_new, - .tp_vectorcall = vc_zeroarg_vectorcall, -}; - /*[clinic input] output push @@ -2667,9 +2636,6 @@ PyInit__testclinic(void) if (PyModule_AddType(m, &VcNewExact_Type) < 0) { goto error; } - if (PyModule_AddType(m, &VcNewZeroArg_Type) < 0) { - goto error; - } return m; error: diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index 29ab6097c64902..9fbb13a2c00b5d 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -5000,133 +5000,4 @@ vc_exact_vectorcall(PyObject *type, PyObject *const *args, exit: return return_value; } - -static PyObject * -vc_zeroarg_new_impl(PyTypeObject *type, PyObject *a, PyObject *b); - -static PyObject * -vc_zeroarg_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) -{ - PyObject *return_value = NULL; - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { _Py_LATIN1_CHR('b'), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"", "b", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "VcNewZeroArg", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[2]; - PyObject * const *fastargs; - Py_ssize_t nargs = PyTuple_GET_SIZE(args); - Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; - PyObject *a = Py_None; - PyObject *b = Py_None; - - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, - /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); - if (!fastargs) { - goto exit; - } - if (nargs < 1) { - goto skip_optional_posonly; - } - noptargs--; - a = fastargs[0]; -skip_optional_posonly: - if (!noptargs) { - goto skip_optional_kwonly; - } - b = fastargs[1]; -skip_optional_kwonly: - return_value = vc_zeroarg_new_impl(type, a, b); - -exit: - return return_value; -} - -static PyObject * -vc_zeroarg_vectorcall(PyObject *type, PyObject *const *args, - size_t nargsf, PyObject *kwnames) -{ - PyObject *return_value = NULL; - Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); - PyObject *a = Py_None; - PyObject *b = Py_None; - - if (nargs == 0 && kwnames == NULL) { - return Py_NewRef(Py_None); - } - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { _Py_LATIN1_CHR('b'), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"", "b", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "VcNewZeroArg", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[2]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, - &_parser, - /*minpos*/ 0, /*maxpos*/ 1, - /*minkw*/ 0, - /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - if (nargs < 1) { - goto skip_optional_posonly_vc; - } - noptargs--; - a = args[0]; -skip_optional_posonly_vc: - if (!noptargs) { - goto skip_optional_kwonly_vc; - } - b = args[1]; -skip_optional_kwonly_vc: - return_value = vc_zeroarg_new_impl(_PyType_CAST(type), a, b); - -exit: - return return_value; -} -/*[clinic end generated code: output=a8d5b01f87544445 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=27755bae91a1cd92 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/tupleobject.c.h b/Objects/clinic/tupleobject.c.h index 937d1764e2d610..1102f641bbd5a4 100644 --- a/Objects/clinic/tupleobject.c.h +++ b/Objects/clinic/tupleobject.c.h @@ -119,9 +119,6 @@ tuple_vectorcall(PyObject *type, PyObject *const *args, Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); PyObject *iterable = NULL; - if (nargs == 0 && kwnames == NULL) { - return (PyObject*)&_Py_SINGLETON(tuple_empty); - } if (!_PyArg_NoKwnames("tuple", kwnames)) { goto exit; } @@ -155,4 +152,4 @@ tuple___getnewargs__(PyObject *self, PyObject *Py_UNUSED(ignored)) { return tuple___getnewargs___impl((PyTupleObject *)self); } -/*[clinic end generated code: output=c1e02d9c2b36d1df input=a9049054013a1b77]*/ +/*[clinic end generated code: output=56cf5ffc37c3e748 input=a9049054013a1b77]*/ diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index ebb3d533f93738..c47b1428712161 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -778,7 +778,7 @@ static PyObject * tuple_subtype_new(PyTypeObject *type, PyObject *iterable); /*[clinic input] -@vectorcall zero_arg=(PyObject*)&_Py_SINGLETON(tuple_empty) +@vectorcall @classmethod tuple.__new__ as tuple_new iterable: object(c_default="NULL") = () @@ -794,7 +794,7 @@ If the argument is a tuple, the return value is the same object. static PyObject * tuple_new_impl(PyTypeObject *type, PyObject *iterable) -/*[clinic end generated code: output=4546d9f0d469bce7 input=fff66d7a13734d92]*/ +/*[clinic end generated code: output=4546d9f0d469bce7 input=8fdda913493ebe48]*/ { if (type != &PyTuple_Type) return tuple_subtype_new(type, iterable); diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 156e9ace4b2e8b..fce4d2ffabad43 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -304,7 +304,6 @@ def reset(self) -> None: self.disable_fastcall = False self.vectorcall = False self.vectorcall_exact_only = False - self.vectorcall_zero_arg = '' self.permit_long_summary = False self.permit_long_docstring_body = False @@ -480,10 +479,6 @@ def at_vectorcall(self, *args: str) -> None: key, value = arg, '' if key == 'exact_only': self.vectorcall_exact_only = True - elif key == 'zero_arg': - if not value: - fail("@vectorcall zero_arg requires a value") - self.vectorcall_zero_arg = value else: fail(f"@vectorcall: unknown argument {key!r}") @@ -751,7 +746,6 @@ def state_modulename_name(self, line: str) -> None: forced_text_signature=self.forced_text_signature, vectorcall=self.vectorcall, vectorcall_exact_only=self.vectorcall_exact_only, - vectorcall_zero_arg=self.vectorcall_zero_arg, ) self.add_function(func) diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index 303d2e0704fddb..b5ff5e7625ec7c 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -113,7 +113,6 @@ class Function: target_critical_section: list[str] = dc.field(default_factory=list) vectorcall: bool = False vectorcall_exact_only: bool = False - vectorcall_zero_arg: str = '' def __post_init__(self) -> None: self.parent = self.cls or self.module diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index b1193abb154cc6..52d51b1e368412 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -1283,22 +1283,11 @@ def generate_vectorcall(self) -> str: }} """) - # Zero-arg shortcut: return a cached value when called with no args - zero_arg_shortcut = "" - if func.vectorcall_zero_arg: - zero_arg_shortcut = libclinic.normalize_snippet(f""" - if (nargs == 0 && kwnames == NULL) {{{{ - return {func.vectorcall_zero_arg}; - }}}} - """, indent=4) - # Assemble the full function lines = [prototype] lines.append(preamble) if exact_check: lines.append(exact_check) - if zero_arg_shortcut: - lines.append(zero_arg_shortcut) lines.extend(parsing_code) lines.append(finale) From e9aff2f6b97250f9a597cb9486427eff421f44ad Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 19 May 2026 15:40:01 -0700 Subject: [PATCH 11/20] Fix lint --- Tools/clinic/libclinic/dsl_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index fce4d2ffabad43..b4d1c7dbf8594c 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -474,9 +474,9 @@ def at_vectorcall(self, *args: str) -> None: self.vectorcall = True for arg in args: if '=' in arg: - key, value = arg.split('=', 1) + key = arg.split('=', 1)[0] else: - key, value = arg, '' + key = arg if key == 'exact_only': self.vectorcall_exact_only = True else: From 446bce5489d92a271f9bfaeea9e20d1a6883ab6e Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 19 May 2026 15:51:00 -0700 Subject: [PATCH 12/20] Move to dataclass for vc args --- Tools/clinic/libclinic/dsl_parser.py | 13 ++++++------- Tools/clinic/libclinic/function.py | 8 ++++++-- Tools/clinic/libclinic/parse_args.py | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index b4d1c7dbf8594c..27ee2db8610bba 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -16,7 +16,7 @@ fail, warn, unspecified, unknown, NULL) from libclinic.function import ( Module, Class, Function, Parameter, - FunctionKind, + FunctionKind, VectorcallOptions, CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, GETTER, SETTER) from libclinic.converter import ( @@ -302,8 +302,7 @@ def reset(self) -> None: self.critical_section = False self.target_critical_section = [] self.disable_fastcall = False - self.vectorcall = False - self.vectorcall_exact_only = False + self.vectorcall: VectorcallOptions | None = None self.permit_long_summary = False self.permit_long_docstring_body = False @@ -469,18 +468,19 @@ def at_staticmethod(self) -> None: self.kind = STATIC_METHOD def at_vectorcall(self, *args: str) -> None: - if self.vectorcall: + if self.vectorcall is not None: fail("Called @vectorcall twice!") - self.vectorcall = True + exact_only = False for arg in args: if '=' in arg: key = arg.split('=', 1)[0] else: key = arg if key == 'exact_only': - self.vectorcall_exact_only = True + exact_only = True else: fail(f"@vectorcall: unknown argument {key!r}") + self.vectorcall = VectorcallOptions(exact_only=exact_only) def at_coexist(self) -> None: if self.coexist: @@ -745,7 +745,6 @@ def state_modulename_name(self, line: str) -> None: target_critical_section=self.target_critical_section, forced_text_signature=self.forced_text_signature, vectorcall=self.vectorcall, - vectorcall_exact_only=self.vectorcall_exact_only, ) self.add_function(func) diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index b5ff5e7625ec7c..dc68cb6aa9a55d 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -78,6 +78,11 @@ def __repr__(self) -> str: SETTER: Final = FunctionKind.SETTER +@dc.dataclass +class VectorcallOptions: + exact_only: bool = False + + @dc.dataclass(repr=False) class Function: """ @@ -111,8 +116,7 @@ class Function: critical_section: bool = False disable_fastcall: bool = False target_critical_section: list[str] = dc.field(default_factory=list) - vectorcall: bool = False - vectorcall_exact_only: bool = False + vectorcall: VectorcallOptions | None = None def __post_init__(self) -> None: self.parent = self.cls or self.module diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 52d51b1e368412..842d70dc43fc1f 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -1229,7 +1229,7 @@ def generate_vectorcall(self) -> str: # Exact type check (if vectorcall_exact_only) exact_check = "" - if func.vectorcall_exact_only and func.cls: + if func.vectorcall and func.vectorcall.exact_only and func.cls: type_obj = func.cls.type_object self.codegen.add_include('pycore_call.h', '_PyObject_MakeTpCall()') From 420d705b103f2f2e622ddc7b382efc2411af3e3b Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 19 May 2026 18:14:36 -0700 Subject: [PATCH 13/20] Fix test failures, refactor to dedup --- Lib/test/test_clinic.py | 6 +- Tools/clinic/libclinic/parse_args.py | 100 ++++++++++++--------------- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 2e0da60add4c34..ca5223cfac49a0 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -2720,7 +2720,7 @@ class Foo "FooObject *" "Foo_Type" func = self.parse_function(block, signatures_in_block=3, function_index=2) self.assertTrue(func.vectorcall) - self.assertFalse(func.vectorcall_exact_only) + self.assertFalse(func.vectorcall.exact_only) def test_vectorcall_on_new(self): block = """ @@ -2735,7 +2735,7 @@ class Foo "FooObject *" "Foo_Type" func = self.parse_function(block, signatures_in_block=3, function_index=2) self.assertTrue(func.vectorcall) - self.assertFalse(func.vectorcall_exact_only) + self.assertFalse(func.vectorcall.exact_only) def test_vectorcall_exact_only(self): block = """ @@ -2749,7 +2749,7 @@ class Foo "FooObject *" "Foo_Type" func = self.parse_function(block, signatures_in_block=3, function_index=2) self.assertTrue(func.vectorcall) - self.assertTrue(func.vectorcall_exact_only) + self.assertTrue(func.vectorcall.exact_only) def test_vectorcall_invalid_kwarg(self): err = "unknown argument" diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 842d70dc43fc1f..0293119eb848ce 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -1075,22 +1075,7 @@ def _generate_vc_parsing_code(self) -> list[str]: def emit(text: str, indent: int = 4) -> None: parser_code.append(snippet(text, indent=indent)) - if no_params: - self.codegen.add_include('pycore_modsupport.h', - '_PyArg_NoKwnames()') - emit(""" - if (!_PyArg_NoKwnames("{name}", kwnames)) {{ - goto exit; - }} - if (nargs != 0) {{ - PyErr_Format(PyExc_TypeError, - "{name}() takes no arguments (%zd given)", - nargs); - goto exit; - }} - """) - return parser_code - elif all_pos_only: + if no_params or all_pos_only: self.codegen.add_include('pycore_modsupport.h', '_PyArg_NoKwnames()') emit(""" @@ -1099,21 +1084,30 @@ def emit(text: str, indent: int = 4) -> None: }} """) + if no_params: + emit(""" + if (nargs != 0) {{ + PyErr_Format(PyExc_TypeError, + "{name}() takes no arguments (%zd given)", + nargs); + goto exit; + }} + """) + return parser_code + pos_code, success = self._generate_vc_pos_only_code() if not success: for parameter in self.parameters: parameter.converter.use_converter() self.codegen.add_include('pycore_modsupport.h', '_PyArg_ParseStack()') - return [snippet(""" - if (!_PyArg_NoKwnames("{name}", kwnames)) {{ - goto exit; - }} + emit(""" if (!_PyArg_ParseStack(args, nargs, "{format_units}:{name}", {parse_arguments})) {{ goto exit; }} - """, indent=4)] + """) + return parser_code parser_code.extend(pos_code) return parser_code else: @@ -1203,23 +1197,16 @@ def emit(text: str, indent: int = 4) -> None: return parser_code - def generate_vectorcall(self) -> str: - """Generate a vectorcall function for __init__ or __new__.""" - func = self.func + def _vc_prototype(self) -> str: vc_basename = self._vc_basename() - - # Generate argument parsing code (FASTCALL-style) - parsing_code = self._generate_vc_parsing_code() - - # Build the function prototype - prototype = libclinic.normalize_snippet(f""" + return libclinic.normalize_snippet(f""" static PyObject * {vc_basename}(PyObject *type, PyObject *const *args, size_t nargsf, PyObject *kwnames) """) - # Build the preamble - preamble = libclinic.normalize_snippet(""" + def _vc_preamble(self) -> str: + return libclinic.normalize_snippet(""" {{ PyObject *return_value = NULL; Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); @@ -1227,23 +1214,23 @@ def generate_vectorcall(self) -> str: {initializers} """) + "\n" - # Exact type check (if vectorcall_exact_only) - exact_check = "" - if func.vectorcall and func.vectorcall.exact_only and func.cls: - type_obj = func.cls.type_object - self.codegen.add_include('pycore_call.h', - '_PyObject_MakeTpCall()') - exact_check = libclinic.normalize_snippet(f""" - if (_PyType_CAST(type) != {type_obj}) {{{{ - PyThreadState *tstate = _PyThreadState_GET(); - return _PyObject_MakeTpCall(tstate, type, args, - nargs, kwnames); - }}}} - """, indent=4) - - # Build the finale (impl call + return) - if func.kind is METHOD_INIT: - finale = libclinic.normalize_snippet(""" + def _vc_exact_check(self) -> str: + func = self.func + if not (func.vectorcall and func.vectorcall.exact_only and func.cls): + return "" + type_obj = func.cls.type_object + self.codegen.add_include('pycore_call.h', '_PyObject_MakeTpCall()') + return libclinic.normalize_snippet(f""" + if (_PyType_CAST(type) != {type_obj}) {{{{ + PyThreadState *tstate = _PyThreadState_GET(); + return _PyObject_MakeTpCall(tstate, type, args, + nargs, kwnames); + }}}} + """, indent=4) + + def _vc_finale(self) -> str: + if self.func.kind is METHOD_INIT: + return libclinic.normalize_snippet(""" {modifications} {lock} {{ @@ -1269,7 +1256,7 @@ def generate_vectorcall(self) -> str: """) else: # METHOD_NEW - finale = libclinic.normalize_snippet(""" + return libclinic.normalize_snippet(""" {modifications} {lock} return_value = {c_basename}_impl({vc_impl_arguments}); @@ -1283,13 +1270,18 @@ def generate_vectorcall(self) -> str: }} """) - # Assemble the full function - lines = [prototype] - lines.append(preamble) + def generate_vectorcall(self) -> str: + """Generate a vectorcall function for __init__ or __new__.""" + parsing_code = self._generate_vc_parsing_code() + + lines = [self._vc_prototype(), self._vc_preamble()] + + exact_check = self._vc_exact_check() if exact_check: lines.append(exact_check) + lines.extend(parsing_code) - lines.append(finale) + lines.append(self._vc_finale()) code = libclinic.linear_format( "\n".join(lines), From e4d18aa59b80f78e01164b4c8224bf18ed8710b5 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 19 May 2026 19:31:14 -0700 Subject: [PATCH 14/20] Simplify now there's only exact_only --- Tools/clinic/libclinic/dsl_parser.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 27ee2db8610bba..a75848951a2ce4 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -470,16 +470,12 @@ def at_staticmethod(self) -> None: def at_vectorcall(self, *args: str) -> None: if self.vectorcall is not None: fail("Called @vectorcall twice!") - exact_only = False - for arg in args: - if '=' in arg: - key = arg.split('=', 1)[0] - else: - key = arg - if key == 'exact_only': - exact_only = True - else: - fail(f"@vectorcall: unknown argument {key!r}") + flags = list(args) + exact_only = 'exact_only' in flags + if exact_only: + flags.remove('exact_only') + if flags: + fail(f"@vectorcall: unknown argument {flags[0]!r}") self.vectorcall = VectorcallOptions(exact_only=exact_only) def at_coexist(self) -> None: From 7e089b0291fd9fa4432031e76e066fadcf7f3b74 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 21 May 2026 15:52:42 -0700 Subject: [PATCH 15/20] Remove removed class from globals-to-fix.tsv --- Tools/c-analyzer/cpython/globals-to-fix.tsv | 1 - 1 file changed, 1 deletion(-) diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 7979fcefdfc092..3bdcb417888456 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -356,7 +356,6 @@ Modules/_testclinic.c - TestClass - Modules/_testclinic.c - VcInit_Type - Modules/_testclinic.c - VcNew_Type - Modules/_testclinic.c - VcNewExact_Type - -Modules/_testclinic.c - VcNewZeroArg_Type - ################################## From 7f56bb0832e237d7b17f41a183f9665a66db9917 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 21 May 2026 17:01:41 -0700 Subject: [PATCH 16/20] Refactor kwarg parsing to be a helper function when emitting two parse functions --- Lib/test/test_clinic.py | 1 + Modules/clinic/_testclinic.c.h | 212 +++++---------- Objects/clinic/enumobject.c.h | 69 ++--- Tools/clinic/libclinic/app.py | 1 + Tools/clinic/libclinic/parse_args.py | 378 ++++++++++++++------------- 5 files changed, 288 insertions(+), 373 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index ca5223cfac49a0..bea91557914898 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -625,6 +625,7 @@ def test_directive_output_invalid_command(self): - 'methoddef_define' - 'impl_prototype' - 'parser_prototype' + - 'parser_helper_definition' - 'parser_definition' - 'vectorcall_definition' - 'cpp_endif' diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index 9fbb13a2c00b5d..826dfeb82ee32e 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -4606,7 +4606,8 @@ static PyObject * vc_plain_new_impl(PyTypeObject *type, PyObject *a); static PyObject * -vc_plain_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +vc_plain_new_parse_args(PyTypeObject *type, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -4638,11 +4639,17 @@ vc_plain_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) #undef KWTUPLE PyObject *argsbuf[1]; PyObject * const *fastargs; - Py_ssize_t nargs = PyTuple_GET_SIZE(args); - Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + Py_ssize_t nkw = 0; + if (kwnames != NULL) { + nkw = PyTuple_GET_SIZE(kwnames); + } + else if (kwargs != NULL) { + nkw = PyDict_GET_SIZE(kwargs); + } + Py_ssize_t noptargs = nargs + nkw - 0; PyObject *a = Py_None; - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!fastargs) { goto exit; @@ -4658,6 +4665,13 @@ vc_plain_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +vc_plain_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return vc_plain_new_parse_args(type, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), kwargs, NULL); +} + static PyObject * vc_plain_vectorcall(PyObject *type, PyObject *const *args, size_t nargsf, PyObject *kwnames) @@ -4677,48 +4691,8 @@ vc_plain_vectorcall(PyObject *type, PyObject *const *args, skip_optional_vc_fast: goto vc_fast_end; } - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { _Py_LATIN1_CHR('a'), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"a", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "VcNew", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[1]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, - &_parser, - /*minpos*/ 0, /*maxpos*/ 1, - /*minkw*/ 0, - /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - if (!noptargs) { - goto skip_optional_pos_vc; - } - a = args[0]; -skip_optional_pos_vc: + return vc_plain_new_parse_args(_PyType_CAST(type), args, + nargs, NULL, kwnames); vc_fast_end: return_value = vc_plain_new_impl(_PyType_CAST(type), a); @@ -4730,7 +4704,8 @@ static int vc_posorkw_init_impl(PyObject *self, PyObject *a, PyObject *b); static int -vc_posorkw_init(PyObject *self, PyObject *args, PyObject *kwargs) +vc_posorkw_init_parse_args(PyObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) { int return_value = -1; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -4762,12 +4737,18 @@ vc_posorkw_init(PyObject *self, PyObject *args, PyObject *kwargs) #undef KWTUPLE PyObject *argsbuf[2]; PyObject * const *fastargs; - Py_ssize_t nargs = PyTuple_GET_SIZE(args); - Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1; + Py_ssize_t nkw = 0; + if (kwnames != NULL) { + nkw = PyTuple_GET_SIZE(kwnames); + } + else if (kwargs != NULL) { + nkw = PyDict_GET_SIZE(kwargs); + } + Py_ssize_t noptargs = nargs + nkw - 1; PyObject *a; PyObject *b = Py_None; - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!fastargs) { goto exit; @@ -4784,6 +4765,13 @@ vc_posorkw_init(PyObject *self, PyObject *args, PyObject *kwargs) return return_value; } +static int +vc_posorkw_init(PyObject *self, PyObject *args, PyObject *kwargs) +{ + return vc_posorkw_init_parse_args(self, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), kwargs, NULL); +} + static PyObject * vc_posorkw_vectorcall(PyObject *type, PyObject *const *args, size_t nargsf, PyObject *kwnames) @@ -4805,49 +4793,20 @@ vc_posorkw_vectorcall(PyObject *type, PyObject *const *args, skip_optional_vc_fast: goto vc_fast_end; } - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { _Py_LATIN1_CHR('b'), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"", "b", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "VcInit", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[2]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, - &_parser, - /*minpos*/ 1, /*maxpos*/ 2, - /*minkw*/ 0, - /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - a = args[0]; - if (!noptargs) { - goto skip_optional_pos_vc; + { + PyObject *self = _PyType_CAST(type)->tp_alloc( + _PyType_CAST(type), 0); + if (self == NULL) { + return NULL; + } + int _result = vc_posorkw_init_parse_args(self, + args, nargs, NULL, kwnames); + if (_result != 0) { + Py_DECREF(self); + return NULL; + } + return self; } - b = args[1]; -skip_optional_pos_vc: vc_fast_end: { PyObject *self = _PyType_CAST(type)->tp_alloc( @@ -4871,7 +4830,8 @@ static PyObject * vc_exact_new_impl(PyTypeObject *type, PyObject *a, PyObject *b); static PyObject * -vc_exact_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +vc_exact_new_parse_args(PyTypeObject *type, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -4903,12 +4863,18 @@ vc_exact_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) #undef KWTUPLE PyObject *argsbuf[2]; PyObject * const *fastargs; - Py_ssize_t nargs = PyTuple_GET_SIZE(args); - Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1; + Py_ssize_t nkw = 0; + if (kwnames != NULL) { + nkw = PyTuple_GET_SIZE(kwnames); + } + else if (kwargs != NULL) { + nkw = PyDict_GET_SIZE(kwargs); + } + Py_ssize_t noptargs = nargs + nkw - 1; PyObject *a; PyObject *b = Py_None; - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!fastargs) { goto exit; @@ -4925,6 +4891,13 @@ vc_exact_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +vc_exact_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return vc_exact_new_parse_args(type, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), kwargs, NULL); +} + static PyObject * vc_exact_vectorcall(PyObject *type, PyObject *const *args, size_t nargsf, PyObject *kwnames) @@ -4951,53 +4924,12 @@ vc_exact_vectorcall(PyObject *type, PyObject *const *args, skip_optional_vc_fast: goto vc_fast_end; } - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { _Py_LATIN1_CHR('b'), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"", "b", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "VcNewExact", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[2]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, - &_parser, - /*minpos*/ 1, /*maxpos*/ 2, - /*minkw*/ 0, - /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - a = args[0]; - if (!noptargs) { - goto skip_optional_pos_vc; - } - b = args[1]; -skip_optional_pos_vc: + return vc_exact_new_parse_args(_PyType_CAST(type), args, + nargs, NULL, kwnames); vc_fast_end: return_value = vc_exact_new_impl(_PyType_CAST(type), a, b); exit: return return_value; } -/*[clinic end generated code: output=27755bae91a1cd92 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=9d74cc1d3fc45354 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/enumobject.c.h b/Objects/clinic/enumobject.c.h index 9092572d1e3ca2..39379a69294241 100644 --- a/Objects/clinic/enumobject.c.h +++ b/Objects/clinic/enumobject.c.h @@ -27,7 +27,8 @@ static PyObject * enum_new_impl(PyTypeObject *type, PyObject *iterable, PyObject *start); static PyObject * -enum_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +enum_new_parse_args(PyTypeObject *type, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -59,12 +60,18 @@ enum_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) #undef KWTUPLE PyObject *argsbuf[2]; PyObject * const *fastargs; - Py_ssize_t nargs = PyTuple_GET_SIZE(args); - Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1; + Py_ssize_t nkw = 0; + if (kwnames != NULL) { + nkw = PyTuple_GET_SIZE(kwnames); + } + else if (kwargs != NULL) { + nkw = PyDict_GET_SIZE(kwargs); + } + Py_ssize_t noptargs = nargs + nkw - 1; PyObject *iterable; PyObject *start = 0; - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!fastargs) { goto exit; @@ -81,6 +88,13 @@ enum_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +enum_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return enum_new_parse_args(type, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), kwargs, NULL); +} + static PyObject * enum_vectorcall(PyObject *type, PyObject *const *args, size_t nargsf, PyObject *kwnames) @@ -102,49 +116,8 @@ enum_vectorcall(PyObject *type, PyObject *const *args, skip_optional_vc_fast: goto vc_fast_end; } - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 2 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { &_Py_ID(iterable), &_Py_ID(start), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"iterable", "start", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "enumerate", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[2]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, - &_parser, - /*minpos*/ 1, /*maxpos*/ 2, - /*minkw*/ 0, - /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - iterable = args[0]; - if (!noptargs) { - goto skip_optional_pos_vc; - } - start = args[1]; -skip_optional_pos_vc: + return enum_new_parse_args(_PyType_CAST(type), args, + nargs, NULL, kwnames); vc_fast_end: return_value = enum_new_impl(_PyType_CAST(type), iterable, start); @@ -202,4 +175,4 @@ reversed_vectorcall(PyObject *type, PyObject *const *args, exit: return return_value; } -/*[clinic end generated code: output=d0c0441d7f42cd54 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=070d18c13ebb3400 input=a9049054013a1b77]*/ diff --git a/Tools/clinic/libclinic/app.py b/Tools/clinic/libclinic/app.py index c8ca4cb452a0e9..a19a4d70a71492 100644 --- a/Tools/clinic/libclinic/app.py +++ b/Tools/clinic/libclinic/app.py @@ -120,6 +120,7 @@ def __init__( 'methoddef_define': d('file'), 'impl_prototype': d('file'), 'parser_prototype': d('suppress'), + 'parser_helper_definition': d('file'), 'parser_definition': d('file'), 'vectorcall_definition': d('file'), 'cpp_endif': d('file'), diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 0293119eb848ce..8b5309011922c0 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -106,6 +106,16 @@ def declare_parser( static int {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs) """) +PARSER_PROTOTYPE_KEYWORD_HELPER: Final[str] = libclinic.normalize_snippet(""" + static PyObject * + {c_basename}_parse_args({self_type}{self_name}, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) +""") +PARSER_PROTOTYPE_KEYWORD___INIT___HELPER: Final[str] = libclinic.normalize_snippet(""" + static int + {c_basename}_parse_args({self_type}{self_name}, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) +""") PARSER_PROTOTYPE_VARARGS: Final[str] = libclinic.normalize_snippet(""" static PyObject * {c_basename}({self_type}{self_name}, PyObject *args) @@ -229,6 +239,7 @@ class ParseArgsCodeGen: methoddef_define: str parser_prototype: str parser_definition: str + parser_helper_definition: str cpp_if: str cpp_endif: str methoddef_ifndef: str @@ -691,6 +702,32 @@ def parse_general(self, clang: CLanguage) -> None: if has_optional_kw: self.declarations += "\nPy_ssize_t noptargs = %s + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - %d;" % (nargs, self.min_pos + self.min_kw_only) unpack_args = 'args, nargs, NULL, kwnames' + elif self.func.vectorcall: + # tp_new / tp_init body emitted as a helper that takes both + # calling conventions; both the tuple/dict entry point and + # the vectorcall entry point call this helper. + self.flags = "METH_VARARGS|METH_KEYWORDS" + if self.func.kind is METHOD_INIT: + self.parser_prototype = PARSER_PROTOTYPE_KEYWORD___INIT___HELPER + else: + self.parser_prototype = PARSER_PROTOTYPE_KEYWORD_HELPER + argsname = 'fastargs' + argname_fmt = 'fastargs[%d]' + self.declarations = declare_parser(self.func, codegen=self.codegen) + self.declarations += "\nPyObject *argsbuf[%s];" % (len(self.converters) or 1) + self.declarations += "\nPyObject * const *fastargs;" + if has_optional_kw: + self.declarations += ( + "\nPy_ssize_t nkw = 0;" + "\nif (kwnames != NULL) {{" + "\n nkw = PyTuple_GET_SIZE(kwnames);" + "\n}}" + "\nelse if (kwargs != NULL) {{" + "\n nkw = PyDict_GET_SIZE(kwargs);" + "\n}}" + "\nPy_ssize_t noptargs = %s + nkw - %d;" + % (nargs, self.min_pos + self.min_kw_only)) + unpack_args = 'args, nargs, kwargs, kwnames' else: # positional-or-keyword arguments self.flags = "METH_VARARGS|METH_KEYWORDS" @@ -724,18 +761,69 @@ def parse_general(self, clang: CLanguage) -> None: fastcall=self.fastcall) parser_code.append(code) - per_arg_code, success = self._generate_keyword_per_arg_parsing( - argname_fmt=argname_fmt, - has_optional_kw=has_optional_kw, - limited_capi=self.limited_capi, - ) - if success: - parser_code.extend(per_arg_code) - else: - parser_code = [] - use_parser_code = False + add_label: str | None = None + for i, p in enumerate(self.parameters): + if isinstance(p.converter, defining_class_converter): + raise ValueError("defining_class should be the first " + "parameter (after clang)") + displayname = p.get_displayname(i+1) + parsearg = p.converter.parse_arg(argname_fmt % i, displayname, limited_capi=self.limited_capi) + if parsearg is None: + parser_code = [] + use_parser_code = False + break + if add_label and (i == self.pos_only or i == self.max_pos): + parser_code.append("%s:" % add_label) + add_label = None + if not p.is_optional(): + parser_code.append(libclinic.normalize_snippet(parsearg, indent=4)) + elif i < self.pos_only: + add_label = 'skip_optional_posonly' + parser_code.append(libclinic.normalize_snippet(""" + if (nargs < %d) {{ + goto %s; + }} + """ % (i + 1, add_label), indent=4)) + if has_optional_kw: + parser_code.append(libclinic.normalize_snippet(""" + noptargs--; + """, indent=4)) + parser_code.append(libclinic.normalize_snippet(parsearg, indent=4)) + else: + if i < self.max_pos: + label = 'skip_optional_pos' + first_opt = max(self.min_pos, self.pos_only) + else: + label = 'skip_optional_kwonly' + first_opt = self.max_pos + self.min_kw_only + if i == first_opt: + add_label = label + parser_code.append(libclinic.normalize_snippet(""" + if (!noptargs) {{ + goto %s; + }} + """ % add_label, indent=4)) + if i + 1 == len(self.parameters): + parser_code.append(libclinic.normalize_snippet(parsearg, indent=4)) + else: + add_label = label + parser_code.append(libclinic.normalize_snippet(""" + if (%s) {{ + """ % (argname_fmt % i), indent=4)) + parser_code.append(libclinic.normalize_snippet(parsearg, indent=8)) + parser_code.append(libclinic.normalize_snippet(""" + if (!--noptargs) {{ + goto %s; + }} + }} + """ % add_label, indent=4)) - if not use_parser_code: + if use_parser_code: + if add_label: + parser_code.append("%s:" % add_label) + if self.varpos: + parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4)) + else: for parameter in self.parameters: parameter.converter.use_converter() @@ -806,14 +894,33 @@ def handle_new_or_init(self) -> None: self.methoddef_define = '' if self.func.kind is METHOD_NEW: - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD + entry_prototype = PARSER_PROTOTYPE_KEYWORD else: self.return_value_declaration = "int return_value = -1;" - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD___INIT__ + entry_prototype = PARSER_PROTOTYPE_KEYWORD___INIT__ + + parses_keywords = 'METH_KEYWORDS' in self.flags + + if self.func.vectorcall and parses_keywords: + # parse_general emitted a helper body into parser_definition; + # move it to parser_helper_definition and replace + # parser_definition with a thin shim that calls the helper. + self.parser_helper_definition = self.parser_definition + self.parser_prototype = entry_prototype + self.parser_definition = '\n'.join([ + entry_prototype, + '{{', + ' return {c_basename}_parse_args({self_name}, ' + '_PyTuple_CAST(args)->ob_item,', + ' PyTuple_GET_SIZE(args), kwargs, NULL);', + '}}', + ]) + return + + self.parser_prototype = entry_prototype fields: list[str] = list(self.parser_body_fields) parses_positional = 'METH_NOARGS' not in self.flags - parses_keywords = 'METH_KEYWORDS' in self.flags if parses_keywords: assert parses_positional @@ -884,6 +991,9 @@ def finalize(self, clang: CLanguage) -> None: self.impl_prototype += ";" self.parser_definition = self.parser_definition.replace("{return_value_declaration}", self.return_value_declaration) + if self.parser_helper_definition: + self.parser_helper_definition = self.parser_helper_definition.replace( + "{return_value_declaration}", self.return_value_declaration) compiler_warning = clang.compiler_deprecated_warning(self.func, self.parameters) if compiler_warning: @@ -897,6 +1007,7 @@ def create_template_dict(self) -> dict[str, str]: "methoddef_define" : self.methoddef_define, "parser_prototype" : self.parser_prototype, "parser_definition" : self.parser_definition, + "parser_helper_definition" : self.parser_helper_definition, "impl_definition" : self.impl_definition, "cpp_if" : self.cpp_if, "cpp_endif" : self.cpp_endif, @@ -928,87 +1039,6 @@ def _vc_basename(self) -> str: break return f'{name}_vectorcall' - def _generate_keyword_per_arg_parsing( - self, - *, - argname_fmt: str, - has_optional_kw: bool, - label_suffix: str = '', - limited_capi: bool = False, - ) -> tuple[list[str], bool]: - """Generate per-argument parsing code for keyword-capable functions. - - Shared between parse_general (FASTCALL|KEYWORDS) and vectorcall - keyword parsing. Returns (code_lines, success). success is False - when a converter doesn't support parse_arg. - """ - code: list[str] = [] - - def emit(text: str, indent: int = 4) -> None: - code.append(libclinic.normalize_snippet(text, indent=indent)) - - add_label: str | None = None - for i, p in enumerate(self.parameters): - if isinstance(p.converter, defining_class_converter): - raise ValueError("defining_class should be the first " - "parameter (after clang)") - displayname = p.get_displayname(i + 1) - parsearg = p.converter.parse_arg( - argname_fmt % i, displayname, limited_capi=limited_capi) - if parsearg is None: - return [], False - if add_label and (i == self.pos_only or i == self.max_pos): - code.append("%s:" % add_label) - add_label = None - if not p.is_optional(): - emit(parsearg) - elif i < self.pos_only: - add_label = f'skip_optional_posonly{label_suffix}' - emit(""" - if (nargs < %d) {{ - goto %s; - }} - """ % (i + 1, add_label)) - if has_optional_kw: - emit(""" - noptargs--; - """) - emit(parsearg) - else: - if i < self.max_pos: - label = f'skip_optional_pos{label_suffix}' - first_opt = max(self.min_pos, self.pos_only) - else: - label = f'skip_optional_kwonly{label_suffix}' - first_opt = self.max_pos + self.min_kw_only - if i == first_opt: - add_label = label - emit(""" - if (!noptargs) {{ - goto %s; - }} - """ % add_label) - if i + 1 == len(self.parameters): - emit(parsearg) - else: - add_label = label - emit(""" - if (%s) {{ - """ % (argname_fmt % i)) - emit(parsearg, indent=8) - emit(""" - if (!--noptargs) {{ - goto %s; - }} - }} - """ % add_label) - - if add_label: - code.append("%s:" % add_label) - if self.varpos: - emit(self._parse_vararg()) - return code, True - def _generate_vc_pos_only_code( self, label_suffix: str = '', @@ -1062,8 +1092,13 @@ def emit(text: str, ind: int = indent) -> None: return code, True - def _generate_vc_parsing_code(self) -> list[str]: - """Generate FASTCALL-style argument parsing code for vectorcall.""" + def _generate_vc_parsing_code(self) -> tuple[list[str], bool]: + """Generate FASTCALL-style argument parsing code for vectorcall. + + Returns ``(parser_code, needs_finale)``. When ``needs_finale`` is + False the parser code already returns from the function (the + impl-call finale would be unreachable and must not be emitted). + """ no_params = (not self.parameters and not self.varpos and not self.var_keyword) all_pos_only = (self.pos_only == len(self.parameters) @@ -1093,7 +1128,7 @@ def emit(text: str, indent: int = 4) -> None: goto exit; }} """) - return parser_code + return parser_code, True pos_code, success = self._generate_vc_pos_only_code() if not success: @@ -1107,95 +1142,61 @@ def emit(text: str, indent: int = 4) -> None: goto exit; }} """) - return parser_code + return parser_code, True parser_code.extend(pos_code) - return parser_code - else: - # General case: has keyword args. Use _PyArg_UnpackKeywords - # in FASTCALL style. - - # Check if we can generate a kwnames==NULL fast path. - # This avoids the overhead of _PyArg_UnpackKeywords when - # only positional args are passed (the common case). - has_kw_only = any(p.is_keyword_only() - for p in self.parameters) - can_fast_path = (not has_kw_only and not self.varpos - and not self.var_keyword) - - if can_fast_path: - fast_code, success = self._generate_vc_pos_only_code( - label_suffix='_fast', indent=8) - if success: - emit(""" - if (kwnames == NULL) {{ - """) - parser_code.extend(fast_code) - emit(""" - goto vc_fast_end; - }} - """) - - self.codegen.add_include('pycore_modsupport.h', - '_PyArg_UnpackKeywords()') - vc_declarations = declare_parser( - self.func, codegen=self.codegen) - vc_declarations += ("\nPyObject *argsbuf[%s];" - % (len(self.converters) or 1)) - - nargs_expr = 'nargs' - if self.varpos: - nargs_expr = (f'Py_MIN(nargs, {self.max_pos})' - if self.max_pos else '0') - - has_optional_kw = ( - max(self.pos_only, self.min_pos) + self.min_kw_only - < len(self.converters) - ) - if has_optional_kw: - vc_declarations += ( - "\nPy_ssize_t noptargs = %s + " - "(kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - %d;" - % (nargs_expr, self.min_pos + self.min_kw_only)) - - emit(vc_declarations) - - emit(f""" - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, - &_parser, - /*minpos*/ {self.min_pos}, /*maxpos*/ {self.max_pos}, - /*minkw*/ {self.min_kw_only}, - /*varpos*/ {1 if self.varpos else 0}, argsbuf); - if (!args) {{{{ - goto exit; - }}}} - """) - - per_arg_code, success = self._generate_keyword_per_arg_parsing( - argname_fmt='args[%d]', - has_optional_kw=has_optional_kw, - label_suffix='_vc', - ) - if not success: - for parameter in self.parameters: - parameter.converter.use_converter() - self.codegen.add_include( - 'pycore_modsupport.h', - '_PyArg_ParseStackAndKeywords()') - return [ - snippet(vc_declarations, indent=4), - snippet(""" - if (!_PyArg_ParseStackAndKeywords(args, nargs, - kwnames, &_parser{parse_arguments_comma} - {parse_arguments})) {{ - goto exit; + return parser_code, True + + # General case: has keyword args. The slow (kwnames != NULL) path + # delegates to the {c_basename}_parse_args helper emitted by + # parse_general. The kwnames == NULL fast path is kept inline so + # the no-kwargs hot path skips _PyArg_UnpackKeywords entirely and + # falls through to the impl-call finale via vc_fast_end. + has_kw_only = any(p.is_keyword_only() for p in self.parameters) + can_fast_path = (not has_kw_only and not self.varpos + and not self.var_keyword) + + emitted_fast_path = False + if can_fast_path: + fast_code, success = self._generate_vc_pos_only_code( + label_suffix='_fast', indent=8) + if success: + emitted_fast_path = True + emit(""" + if (kwnames == NULL) {{ + """) + parser_code.extend(fast_code) + emit(""" + goto vc_fast_end; }} - """, indent=4)] - parser_code.extend(per_arg_code) + """) - if can_fast_path: - parser_code.append("vc_fast_end:") + if self.func.kind is METHOD_INIT: + emit(""" + {{ + PyObject *self = _PyType_CAST(type)->tp_alloc( + _PyType_CAST(type), 0); + if (self == NULL) {{ + return NULL; + }} + int _result = {c_basename}_parse_args(self, + args, nargs, NULL, kwnames); + if (_result != 0) {{ + Py_DECREF(self); + return NULL; + }} + return self; + }} + """) + else: + emit(""" + return {c_basename}_parse_args(_PyType_CAST(type), args, + nargs, NULL, kwnames); + """) - return parser_code + if emitted_fast_path: + parser_code.append("vc_fast_end:") + return parser_code, True + return parser_code, False def _vc_prototype(self) -> str: vc_basename = self._vc_basename() @@ -1272,7 +1273,7 @@ def _vc_finale(self) -> str: def generate_vectorcall(self) -> str: """Generate a vectorcall function for __init__ or __new__.""" - parsing_code = self._generate_vc_parsing_code() + parsing_code, needs_finale = self._generate_vc_parsing_code() lines = [self._vc_prototype(), self._vc_preamble()] @@ -1281,7 +1282,13 @@ def generate_vectorcall(self) -> str: lines.append(exact_check) lines.extend(parsing_code) - lines.append(self._vc_finale()) + if needs_finale: + lines.append(self._vc_finale()) + else: + # Slow path already returned via the helper call; close the + # function so the impl-call finale would not be emitted as + # dead code. + lines.append("}") code = libclinic.linear_format( "\n".join(lines), @@ -1296,6 +1303,7 @@ def parse_args(self, clang: CLanguage) -> dict[str, str]: self.declarations = "" self.parser_prototype = "" self.parser_definition = "" + self.parser_helper_definition = "" self.impl_prototype = None self.impl_definition = IMPL_DEFINITION_PROTOTYPE self.vectorcall_definition = "" From 61ada4520d0e220408ae359fba5d9a1794b8eee1 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 21 May 2026 17:46:29 -0700 Subject: [PATCH 17/20] Add a test class where all args are keyword allowed to increase coverage of generated cases --- Lib/test/test_clinic.py | 15 ++++ Modules/_testclinic.c | 36 +++++++++ Modules/clinic/_testclinic.c.h | 84 ++++++++++++++++++++- Tools/c-analyzer/cpython/globals-to-fix.tsv | 1 + Tools/clinic/libclinic/parse_args.py | 13 +++- 5 files changed, 145 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index bea91557914898..d952fd22c147e4 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -4444,6 +4444,21 @@ def test_vc_new_exact_subclass(self): self.assertIsInstance(obj, Sub) self.assertIsInstance(obj, ac_tester.VcNewExact) + def test_vc_kwonly(self): + # keyword-only 'b': vectorcall has no kwnames==NULL fast path, + # so every call goes through the helper. + self.assertIsInstance(ac_tester.VcKwOnly(1), ac_tester.VcKwOnly) + self.assertIsInstance(ac_tester.VcKwOnly(1, b=2), ac_tester.VcKwOnly) + self.assertIsInstance(ac_tester.VcKwOnly(a=1, b=2), ac_tester.VcKwOnly) + + def test_vc_kwonly_b_as_positional(self): + with self.assertRaises(TypeError): + ac_tester.VcKwOnly(1, 2) + + def test_vc_kwonly_missing_required(self): + with self.assertRaises(TypeError): + ac_tester.VcKwOnly() + class LimitedCAPIOutputTests(unittest.TestCase): diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index a35d7447486045..19185b793420b2 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -26,6 +26,7 @@ custom_converter(PyObject *obj, custom_t *val) static PyTypeObject VcNew_Type; static PyTypeObject VcInit_Type; static PyTypeObject VcNewExact_Type; +static PyTypeObject VcKwOnly_Type; #include "clinic/_testclinic.c.h" @@ -2407,6 +2408,38 @@ static PyTypeObject VcNewExact_Type = { }; +/* VcKwOnly: @vectorcall + keyword-only arg. + * Exercises the no-kwnames==NULL-fast-path branch of the vectorcall codegen: + * the vectorcall function delegates unconditionally to the helper because the + * keyword-only parameter rules out the positional-only fast path. */ + +/*[clinic input] +class _testclinic.VcKwOnly "PyObject *" "&VcKwOnly_Type" +@classmethod +@vectorcall +_testclinic.VcKwOnly.__new__ as vc_kwonly_new + a: object + * + b: object = None +[clinic start generated code]*/ + +static PyObject * +vc_kwonly_new_impl(PyTypeObject *type, PyObject *a, PyObject *b) +/*[clinic end generated code: output=00417079caa234dc input=68c863b55575a9e1]*/ +{ + return type->tp_alloc(type, 0); +} + +static PyTypeObject VcKwOnly_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_testclinic.VcKwOnly", + .tp_basicsize = sizeof(PyObject), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = vc_kwonly_new, + .tp_vectorcall = vc_kwonly_vectorcall, +}; + + /*[clinic input] output push @@ -2636,6 +2669,9 @@ PyInit__testclinic(void) if (PyModule_AddType(m, &VcNewExact_Type) < 0) { goto error; } + if (PyModule_AddType(m, &VcKwOnly_Type) < 0) { + goto error; + } return m; error: diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index 826dfeb82ee32e..0f64d09a170b66 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -4932,4 +4932,86 @@ vc_exact_vectorcall(PyObject *type, PyObject *const *args, exit: return return_value; } -/*[clinic end generated code: output=9d74cc1d3fc45354 input=a9049054013a1b77]*/ + +static PyObject * +vc_kwonly_new_impl(PyTypeObject *type, PyObject *a, PyObject *b); + +static PyObject * +vc_kwonly_new_parse_args(PyTypeObject *type, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('a'), _Py_LATIN1_CHR('b'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"a", "b", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "VcKwOnly", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + PyObject * const *fastargs; + Py_ssize_t nkw = 0; + if (kwnames != NULL) { + nkw = PyTuple_GET_SIZE(kwnames); + } + else if (kwargs != NULL) { + nkw = PyDict_GET_SIZE(kwargs); + } + Py_ssize_t noptargs = nargs + nkw - 1; + PyObject *a; + PyObject *b = Py_None; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + a = fastargs[0]; + if (!noptargs) { + goto skip_optional_kwonly; + } + b = fastargs[1]; +skip_optional_kwonly: + return_value = vc_kwonly_new_impl(type, a, b); + +exit: + return return_value; +} + +static PyObject * +vc_kwonly_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return vc_kwonly_new_parse_args(type, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), kwargs, NULL); +} + +static PyObject * +vc_kwonly_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + + return vc_kwonly_new_parse_args(_PyType_CAST(type), args, + nargs, NULL, kwnames); +} +/*[clinic end generated code: output=f50fc4e8fc5d3848 input=a9049054013a1b77]*/ diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 3bdcb417888456..ae4bc8183c9643 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -354,6 +354,7 @@ Modules/_testclinic.c - DeprKwdInitNoInline - Modules/_testclinic.c - DeprKwdNew - Modules/_testclinic.c - TestClass - Modules/_testclinic.c - VcInit_Type - +Modules/_testclinic.c - VcKwOnly_Type - Modules/_testclinic.c - VcNew_Type - Modules/_testclinic.c - VcNewExact_Type - diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 8b5309011922c0..b83e79187723e6 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -1206,7 +1206,14 @@ def _vc_prototype(self) -> str: size_t nargsf, PyObject *kwnames) """) - def _vc_preamble(self) -> str: + def _vc_preamble(self, needs_finale: bool) -> str: + if not needs_finale: + # No fast path, no impl call: return_value and the per-arg + # locals are unused — emit only what the helper call needs. + return libclinic.normalize_snippet(""" + {{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + """) + "\n" return libclinic.normalize_snippet(""" {{ PyObject *return_value = NULL; @@ -1275,7 +1282,7 @@ def generate_vectorcall(self) -> str: """Generate a vectorcall function for __init__ or __new__.""" parsing_code, needs_finale = self._generate_vc_parsing_code() - lines = [self._vc_prototype(), self._vc_preamble()] + lines = [self._vc_prototype(), self._vc_preamble(needs_finale)] exact_check = self._vc_exact_check() if exact_check: @@ -1288,7 +1295,7 @@ def generate_vectorcall(self) -> str: # Slow path already returned via the helper call; close the # function so the impl-call finale would not be emitted as # dead code. - lines.append("}") + lines.append("}}") code = libclinic.linear_format( "\n".join(lines), From b1cd7b6e1e81cd4e61c5d88976dafd8487dfa803 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 21 May 2026 17:50:45 -0700 Subject: [PATCH 18/20] remove unnecessary branch in keyword parsing helper --- Modules/clinic/_testclinic.c.h | 74 +++++++++++----------------- Objects/clinic/enumobject.c.h | 20 +++----- Tools/clinic/libclinic/parse_args.py | 25 +++++----- 3 files changed, 48 insertions(+), 71 deletions(-) diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index 0f64d09a170b66..3450828cf2450a 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -4607,7 +4607,7 @@ vc_plain_new_impl(PyTypeObject *type, PyObject *a); static PyObject * vc_plain_new_parse_args(PyTypeObject *type, PyObject *const *args, - Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -4639,13 +4639,6 @@ vc_plain_new_parse_args(PyTypeObject *type, PyObject *const *args, #undef KWTUPLE PyObject *argsbuf[1]; PyObject * const *fastargs; - Py_ssize_t nkw = 0; - if (kwnames != NULL) { - nkw = PyTuple_GET_SIZE(kwnames); - } - else if (kwargs != NULL) { - nkw = PyDict_GET_SIZE(kwargs); - } Py_ssize_t noptargs = nargs + nkw - 0; PyObject *a = Py_None; @@ -4669,7 +4662,9 @@ static PyObject * vc_plain_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { return vc_plain_new_parse_args(type, _PyTuple_CAST(args)->ob_item, - PyTuple_GET_SIZE(args), kwargs, NULL); + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); } static PyObject * @@ -4691,8 +4686,9 @@ vc_plain_vectorcall(PyObject *type, PyObject *const *args, skip_optional_vc_fast: goto vc_fast_end; } - return vc_plain_new_parse_args(_PyType_CAST(type), args, - nargs, NULL, kwnames); + return vc_plain_new_parse_args(_PyType_CAST(type), args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); vc_fast_end: return_value = vc_plain_new_impl(_PyType_CAST(type), a); @@ -4705,7 +4701,7 @@ vc_posorkw_init_impl(PyObject *self, PyObject *a, PyObject *b); static int vc_posorkw_init_parse_args(PyObject *self, PyObject *const *args, - Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) { int return_value = -1; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -4737,13 +4733,6 @@ vc_posorkw_init_parse_args(PyObject *self, PyObject *const *args, #undef KWTUPLE PyObject *argsbuf[2]; PyObject * const *fastargs; - Py_ssize_t nkw = 0; - if (kwnames != NULL) { - nkw = PyTuple_GET_SIZE(kwnames); - } - else if (kwargs != NULL) { - nkw = PyDict_GET_SIZE(kwargs); - } Py_ssize_t noptargs = nargs + nkw - 1; PyObject *a; PyObject *b = Py_None; @@ -4769,7 +4758,9 @@ static int vc_posorkw_init(PyObject *self, PyObject *args, PyObject *kwargs) { return vc_posorkw_init_parse_args(self, _PyTuple_CAST(args)->ob_item, - PyTuple_GET_SIZE(args), kwargs, NULL); + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); } static PyObject * @@ -4799,8 +4790,9 @@ vc_posorkw_vectorcall(PyObject *type, PyObject *const *args, if (self == NULL) { return NULL; } - int _result = vc_posorkw_init_parse_args(self, - args, nargs, NULL, kwnames); + int _result = vc_posorkw_init_parse_args(self, args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); if (_result != 0) { Py_DECREF(self); return NULL; @@ -4831,7 +4823,7 @@ vc_exact_new_impl(PyTypeObject *type, PyObject *a, PyObject *b); static PyObject * vc_exact_new_parse_args(PyTypeObject *type, PyObject *const *args, - Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -4863,13 +4855,6 @@ vc_exact_new_parse_args(PyTypeObject *type, PyObject *const *args, #undef KWTUPLE PyObject *argsbuf[2]; PyObject * const *fastargs; - Py_ssize_t nkw = 0; - if (kwnames != NULL) { - nkw = PyTuple_GET_SIZE(kwnames); - } - else if (kwargs != NULL) { - nkw = PyDict_GET_SIZE(kwargs); - } Py_ssize_t noptargs = nargs + nkw - 1; PyObject *a; PyObject *b = Py_None; @@ -4895,7 +4880,9 @@ static PyObject * vc_exact_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { return vc_exact_new_parse_args(type, _PyTuple_CAST(args)->ob_item, - PyTuple_GET_SIZE(args), kwargs, NULL); + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); } static PyObject * @@ -4924,8 +4911,9 @@ vc_exact_vectorcall(PyObject *type, PyObject *const *args, skip_optional_vc_fast: goto vc_fast_end; } - return vc_exact_new_parse_args(_PyType_CAST(type), args, - nargs, NULL, kwnames); + return vc_exact_new_parse_args(_PyType_CAST(type), args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); vc_fast_end: return_value = vc_exact_new_impl(_PyType_CAST(type), a, b); @@ -4938,7 +4926,7 @@ vc_kwonly_new_impl(PyTypeObject *type, PyObject *a, PyObject *b); static PyObject * vc_kwonly_new_parse_args(PyTypeObject *type, PyObject *const *args, - Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -4970,13 +4958,6 @@ vc_kwonly_new_parse_args(PyTypeObject *type, PyObject *const *args, #undef KWTUPLE PyObject *argsbuf[2]; PyObject * const *fastargs; - Py_ssize_t nkw = 0; - if (kwnames != NULL) { - nkw = PyTuple_GET_SIZE(kwnames); - } - else if (kwargs != NULL) { - nkw = PyDict_GET_SIZE(kwargs); - } Py_ssize_t noptargs = nargs + nkw - 1; PyObject *a; PyObject *b = Py_None; @@ -5002,7 +4983,9 @@ static PyObject * vc_kwonly_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { return vc_kwonly_new_parse_args(type, _PyTuple_CAST(args)->ob_item, - PyTuple_GET_SIZE(args), kwargs, NULL); + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); } static PyObject * @@ -5011,7 +4994,8 @@ vc_kwonly_vectorcall(PyObject *type, PyObject *const *args, { Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); - return vc_kwonly_new_parse_args(_PyType_CAST(type), args, - nargs, NULL, kwnames); + return vc_kwonly_new_parse_args(_PyType_CAST(type), args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); } -/*[clinic end generated code: output=f50fc4e8fc5d3848 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e05b3fb47b594279 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/enumobject.c.h b/Objects/clinic/enumobject.c.h index 39379a69294241..a168e83c716a49 100644 --- a/Objects/clinic/enumobject.c.h +++ b/Objects/clinic/enumobject.c.h @@ -28,7 +28,7 @@ enum_new_impl(PyTypeObject *type, PyObject *iterable, PyObject *start); static PyObject * enum_new_parse_args(PyTypeObject *type, PyObject *const *args, - Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -60,13 +60,6 @@ enum_new_parse_args(PyTypeObject *type, PyObject *const *args, #undef KWTUPLE PyObject *argsbuf[2]; PyObject * const *fastargs; - Py_ssize_t nkw = 0; - if (kwnames != NULL) { - nkw = PyTuple_GET_SIZE(kwnames); - } - else if (kwargs != NULL) { - nkw = PyDict_GET_SIZE(kwargs); - } Py_ssize_t noptargs = nargs + nkw - 1; PyObject *iterable; PyObject *start = 0; @@ -92,7 +85,9 @@ static PyObject * enum_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { return enum_new_parse_args(type, _PyTuple_CAST(args)->ob_item, - PyTuple_GET_SIZE(args), kwargs, NULL); + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); } static PyObject * @@ -116,8 +111,9 @@ enum_vectorcall(PyObject *type, PyObject *const *args, skip_optional_vc_fast: goto vc_fast_end; } - return enum_new_parse_args(_PyType_CAST(type), args, - nargs, NULL, kwnames); + return enum_new_parse_args(_PyType_CAST(type), args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); vc_fast_end: return_value = enum_new_impl(_PyType_CAST(type), iterable, start); @@ -175,4 +171,4 @@ reversed_vectorcall(PyObject *type, PyObject *const *args, exit: return return_value; } -/*[clinic end generated code: output=070d18c13ebb3400 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e72fb89486919388 input=a9049054013a1b77]*/ diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index b83e79187723e6..8802d77e0013d1 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -109,12 +109,12 @@ def declare_parser( PARSER_PROTOTYPE_KEYWORD_HELPER: Final[str] = libclinic.normalize_snippet(""" static PyObject * {c_basename}_parse_args({self_type}{self_name}, PyObject *const *args, - Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) """) PARSER_PROTOTYPE_KEYWORD___INIT___HELPER: Final[str] = libclinic.normalize_snippet(""" static int {c_basename}_parse_args({self_type}{self_name}, PyObject *const *args, - Py_ssize_t nargs, PyObject *kwargs, PyObject *kwnames) + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) """) PARSER_PROTOTYPE_VARARGS: Final[str] = libclinic.normalize_snippet(""" static PyObject * @@ -718,13 +718,6 @@ def parse_general(self, clang: CLanguage) -> None: self.declarations += "\nPyObject * const *fastargs;" if has_optional_kw: self.declarations += ( - "\nPy_ssize_t nkw = 0;" - "\nif (kwnames != NULL) {{" - "\n nkw = PyTuple_GET_SIZE(kwnames);" - "\n}}" - "\nelse if (kwargs != NULL) {{" - "\n nkw = PyDict_GET_SIZE(kwargs);" - "\n}}" "\nPy_ssize_t noptargs = %s + nkw - %d;" % (nargs, self.min_pos + self.min_kw_only)) unpack_args = 'args, nargs, kwargs, kwnames' @@ -912,7 +905,9 @@ def handle_new_or_init(self) -> None: '{{', ' return {c_basename}_parse_args({self_name}, ' '_PyTuple_CAST(args)->ob_item,', - ' PyTuple_GET_SIZE(args), kwargs, NULL);', + ' PyTuple_GET_SIZE(args),', + ' kwargs ? PyDict_GET_SIZE(kwargs) : 0,', + ' kwargs, NULL);', '}}', ]) return @@ -1178,8 +1173,9 @@ def emit(text: str, indent: int = 4) -> None: if (self == NULL) {{ return NULL; }} - int _result = {c_basename}_parse_args(self, - args, nargs, NULL, kwnames); + int _result = {c_basename}_parse_args(self, args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); if (_result != 0) {{ Py_DECREF(self); return NULL; @@ -1189,8 +1185,9 @@ def emit(text: str, indent: int = 4) -> None: """) else: emit(""" - return {c_basename}_parse_args(_PyType_CAST(type), args, - nargs, NULL, kwnames); + return {c_basename}_parse_args(_PyType_CAST(type), args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); """) if emitted_fast_path: From 2a7c068efd42b0e0af66409dd6e8074939da1315 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 21 May 2026 18:03:20 -0700 Subject: [PATCH 19/20] de-duplicate prototypes --- Tools/clinic/libclinic/parse_args.py | 36 ++++++++++------------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 8802d77e0013d1..21a605cd0c0f75 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -5,7 +5,7 @@ from libclinic import fail, warn from libclinic.function import ( Function, Parameter, - GETTER, SETTER, METHOD_NEW, METHOD_INIT) + GETTER, SETTER, METHOD_INIT) from libclinic.converter import CConverter from libclinic.converters import ( defining_class_converter, object_converter, self_converter) @@ -99,20 +99,11 @@ def declare_parser( NO_VARARG: Final[str] = "PY_SSIZE_T_MAX" PARSER_PROTOTYPE_KEYWORD: Final[str] = libclinic.normalize_snippet(""" - static PyObject * - {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs) -""") -PARSER_PROTOTYPE_KEYWORD___INIT__: Final[str] = libclinic.normalize_snippet(""" - static int + static {return_type} {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs) """) PARSER_PROTOTYPE_KEYWORD_HELPER: Final[str] = libclinic.normalize_snippet(""" - static PyObject * - {c_basename}_parse_args({self_type}{self_name}, PyObject *const *args, - Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) -""") -PARSER_PROTOTYPE_KEYWORD___INIT___HELPER: Final[str] = libclinic.normalize_snippet(""" - static int + static {return_type} {c_basename}_parse_args({self_type}{self_name}, PyObject *const *args, Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) """) @@ -357,6 +348,10 @@ def init_limited_capi(self) -> None: warn(f"Function {self.func.full_name} cannot use limited C API") self.limited_capi = False + def _keyword_prototype(self, template: str) -> str: + return_type = "int" if self.func.kind is METHOD_INIT else "PyObject *" + return template.replace("{return_type}", return_type) + def parser_body( self, *fields: str, @@ -623,7 +618,7 @@ def parse_pos_only(self) -> None: def parse_var_keyword(self) -> None: self.flags = "METH_VARARGS|METH_KEYWORDS" - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD + self.parser_prototype = self._keyword_prototype(PARSER_PROTOTYPE_KEYWORD) nargs = 'PyTuple_GET_SIZE(args)' parser_code = [] @@ -707,10 +702,7 @@ def parse_general(self, clang: CLanguage) -> None: # calling conventions; both the tuple/dict entry point and # the vectorcall entry point call this helper. self.flags = "METH_VARARGS|METH_KEYWORDS" - if self.func.kind is METHOD_INIT: - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD___INIT___HELPER - else: - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD_HELPER + self.parser_prototype = self._keyword_prototype(PARSER_PROTOTYPE_KEYWORD_HELPER) argsname = 'fastargs' argname_fmt = 'fastargs[%d]' self.declarations = declare_parser(self.func, codegen=self.codegen) @@ -724,7 +716,7 @@ def parse_general(self, clang: CLanguage) -> None: else: # positional-or-keyword arguments self.flags = "METH_VARARGS|METH_KEYWORDS" - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD + self.parser_prototype = self._keyword_prototype(PARSER_PROTOTYPE_KEYWORD) argsname = 'fastargs' argname_fmt = 'fastargs[%d]' self.declarations = declare_parser(self.func, codegen=self.codegen) @@ -826,7 +818,7 @@ def parse_general(self, clang: CLanguage) -> None: # positional-or-keyword arguments assert not self.fastcall self.flags = "METH_VARARGS|METH_KEYWORDS" - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD + self.parser_prototype = self._keyword_prototype(PARSER_PROTOTYPE_KEYWORD) parser_code = [libclinic.normalize_snippet(""" if (!PyArg_ParseTupleAndKeywords(args, kwargs, "{format_units}:{name}", _keywords, {parse_arguments})) @@ -886,11 +878,9 @@ def copy_includes(self) -> None: def handle_new_or_init(self) -> None: self.methoddef_define = '' - if self.func.kind is METHOD_NEW: - entry_prototype = PARSER_PROTOTYPE_KEYWORD - else: + if self.func.kind is METHOD_INIT: self.return_value_declaration = "int return_value = -1;" - entry_prototype = PARSER_PROTOTYPE_KEYWORD___INIT__ + entry_prototype = self._keyword_prototype(PARSER_PROTOTYPE_KEYWORD) parses_keywords = 'METH_KEYWORDS' in self.flags From a19e9a8acb1046d05ebd4dcc8a48e538f12b4f07 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 21 May 2026 18:24:50 -0700 Subject: [PATCH 20/20] Exercise critical section generation in testclinic --- Modules/_testclinic.c | 7 +++++-- Modules/clinic/_testclinic.c.h | 10 ++++++++-- Modules/clinic/_testclinic_depr.c.h | 3 ++- Modules/clinic/_testclinic_kwds.c.h | 3 ++- Tools/clinic/libclinic/parse_args.py | 5 +++-- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index 19185b793420b2..2fc3a99ada65df 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -2350,11 +2350,14 @@ static PyTypeObject VcNew_Type = { }; -/* VcInit: __init__ with one required positional-only and one optional keyword arg */ +/* VcInit: __init__ with one required positional-only and one optional keyword + * arg. Uses @critical_section to exercise the {lock}/impl/{unlock} placement + * in both the helper body and the vectorcall fast-path inner block. */ /*[clinic input] class _testclinic.VcInit "PyObject *" "&VcInit_Type" @vectorcall +@critical_section _testclinic.VcInit.__init__ as vc_posorkw_init a: object / @@ -2363,7 +2366,7 @@ _testclinic.VcInit.__init__ as vc_posorkw_init static int vc_posorkw_init_impl(PyObject *self, PyObject *a, PyObject *b) -/*[clinic end generated code: output=6018424ba9fb0744 input=25e4c2b792040c31]*/ +/*[clinic end generated code: output=6018424ba9fb0744 input=7a4513f78dd42b57]*/ { return 0; } diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index 3450828cf2450a..a04c7107570dde 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -7,6 +7,7 @@ preserve #endif #include "pycore_abstract.h" // _PyNumber_Index() #include "pycore_call.h" // _PyObject_MakeTpCall() +#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION() #include "pycore_long.h" // _PyLong_UnsignedShort_Converter() #include "pycore_modsupport.h" // _PyArg_CheckPositional() #include "pycore_runtime.h" // _Py_ID() @@ -4748,7 +4749,9 @@ vc_posorkw_init_parse_args(PyObject *self, PyObject *const *args, } b = fastargs[1]; skip_optional_pos: + Py_BEGIN_CRITICAL_SECTION(self); return_value = vc_posorkw_init_impl(self, a, b); + Py_END_CRITICAL_SECTION(); exit: return return_value; @@ -4806,7 +4809,10 @@ vc_posorkw_vectorcall(PyObject *type, PyObject *const *args, if (self == NULL) { goto exit; } - int _result = vc_posorkw_init_impl((PyObject *)self, a, b); + int _result; + Py_BEGIN_CRITICAL_SECTION(self); + _result = vc_posorkw_init_impl((PyObject *)self, a, b); + Py_END_CRITICAL_SECTION(); if (_result != 0) { Py_DECREF(self); goto exit; @@ -4998,4 +5004,4 @@ vc_kwonly_vectorcall(PyObject *type, PyObject *const *args, kwnames ? PyTuple_GET_SIZE(kwnames) : 0, NULL, kwnames); } -/*[clinic end generated code: output=e05b3fb47b594279 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=23aef355930eeb8f input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_testclinic_depr.c.h b/Modules/clinic/_testclinic_depr.c.h index 4434bb71207c19..71429aebcea60f 100644 --- a/Modules/clinic/_testclinic_depr.c.h +++ b/Modules/clinic/_testclinic_depr.c.h @@ -7,6 +7,7 @@ preserve #endif #include "pycore_abstract.h" // _PyNumber_Index() #include "pycore_call.h" // _PyObject_MakeTpCall() +#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION() #include "pycore_long.h" // _PyLong_UnsignedShort_Converter() #include "pycore_modsupport.h" // _PyArg_CheckPositional() #include "pycore_runtime.h" // _Py_ID() @@ -2475,4 +2476,4 @@ depr_multi(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject * exit: return return_value; } -/*[clinic end generated code: output=068be10cbdafd91e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=361b43888086f332 input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_testclinic_kwds.c.h b/Modules/clinic/_testclinic_kwds.c.h index 1f92727249f950..7b545f459bc1e6 100644 --- a/Modules/clinic/_testclinic_kwds.c.h +++ b/Modules/clinic/_testclinic_kwds.c.h @@ -7,6 +7,7 @@ preserve #endif #include "pycore_abstract.h" // _PyNumber_Index() #include "pycore_call.h" // _PyObject_MakeTpCall() +#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION() #include "pycore_long.h" // _PyLong_UnsignedShort_Converter() #include "pycore_modsupport.h" // _PyArg_CheckPositional() #include "pycore_runtime.h" // _Py_ID() @@ -182,4 +183,4 @@ kwds_with_pos_only_and_stararg(PyObject *module, PyObject *args, PyObject *kwarg return return_value; } -/*[clinic end generated code: output=937ae7c4a6176de1 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=f52d38b984314a55 input=a9049054013a1b77]*/ diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 21a605cd0c0f75..3e73e6d550efc5 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -1227,14 +1227,15 @@ def _vc_finale(self) -> str: if self.func.kind is METHOD_INIT: return libclinic.normalize_snippet(""" {modifications} - {lock} {{ PyObject *self = _PyType_CAST(type)->tp_alloc( _PyType_CAST(type), 0); if (self == NULL) {{ goto exit; }} - int _result = {c_basename}_impl({vc_impl_arguments}); + int _result; + {lock} + _result = {c_basename}_impl({vc_impl_arguments}); {unlock} if (_result != 0) {{ Py_DECREF(self);