diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 93c284e58764f4..d952fd22c147e4 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -625,7 +625,9 @@ def test_directive_output_invalid_command(self): - 'methoddef_define' - 'impl_prototype' - 'parser_prototype' + - 'parser_helper_definition' - 'parser_definition' + - 'vectorcall_definition' - 'cpp_endif' - 'methoddef_ifndef' - 'impl_definition' @@ -2677,6 +2679,89 @@ 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_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 @@ -4317,6 +4402,64 @@ 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(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(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): + 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(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): + 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_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): def setUp(self): 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. diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index 66a375589ba38e..2fc3a99ada65df 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -21,6 +21,12 @@ 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 VcKwOnly_Type; #include "clinic/_testclinic.c.h" @@ -2315,6 +2321,129 @@ output pop /*[clinic end generated code: output=da39a3ee5e6b4b0d input=e7c7c42daced52b0]*/ +/* @vectorcall test types. One type per exemplar because tp_vectorcall is a single slot. */ + +/* VcNew: __new__ with one optional positional-or-keyword arg */ + +/*[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: __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 + / + 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=7a4513f78dd42b57]*/ +{ + 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: __new__ with exact_only; subclasses fall back to tp_new */ + +/*[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, +}; + + +/* 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 destination kwarg new file '{dirname}/clinic/_testclinic_kwds.c.h' @@ -2534,6 +2663,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, &VcKwOnly_Type) < 0) { + goto error; + } return m; error: diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index 05615c1fdd81b9..a04c7107570dde 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -6,6 +6,8 @@ preserve # include "pycore_gc.h" // PyGC_Head #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() @@ -4600,4 +4602,406 @@ _testclinic_TestClass_posonly_poskw_varpos_array_no_fastcall(PyObject *type, PyO exit: return return_value; } -/*[clinic end generated code: output=9971dbbc5f62b8d2 input=a9049054013a1b77]*/ + +static PyObject * +vc_plain_new_impl(PyTypeObject *type, PyObject *a); + +static PyObject * +vc_plain_new_parse_args(PyTypeObject *type, PyObject *const *args, + 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) + + #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 noptargs = nargs + nkw - 0; + PyObject *a = Py_None; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_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_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return vc_plain_new_parse_args(type, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +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; + } + 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); + +exit: + return return_value; +} + +static int +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, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) +{ + 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 noptargs = nargs + nkw - 1; + PyObject *a; + PyObject *b = Py_None; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_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: + Py_BEGIN_CRITICAL_SECTION(self); + return_value = vc_posorkw_init_impl(self, a, b); + Py_END_CRITICAL_SECTION(); + +exit: + 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 ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +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; + } + { + 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, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); + if (_result != 0) { + Py_DECREF(self); + return NULL; + } + return self; + } +vc_fast_end: + { + PyObject *self = _PyType_CAST(type)->tp_alloc( + _PyType_CAST(type), 0); + if (self == NULL) { + goto exit; + } + 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; + } + return_value = self; + } + +exit: + return return_value; +} + +static PyObject * +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, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) +{ + 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 noptargs = nargs + nkw - 1; + PyObject *a; + PyObject *b = Py_None; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_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_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return vc_exact_new_parse_args(type, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +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; + } + 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); + +exit: + return return_value; +} + +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, Py_ssize_t nkw, 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 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 ? PyDict_GET_SIZE(kwargs) : 0, + 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, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); +} +/*[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 e2db4fd87ed26b..71429aebcea60f 100644 --- a/Modules/clinic/_testclinic_depr.c.h +++ b/Modules/clinic/_testclinic_depr.c.h @@ -6,6 +6,8 @@ preserve # include "pycore_gc.h" // PyGC_Head #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() @@ -2474,4 +2476,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=361b43888086f332 input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_testclinic_kwds.c.h b/Modules/clinic/_testclinic_kwds.c.h index 86cad50c56cf55..7b545f459bc1e6 100644 --- a/Modules/clinic/_testclinic_kwds.c.h +++ b/Modules/clinic/_testclinic_kwds.c.h @@ -6,6 +6,8 @@ preserve # include "pycore_gc.h" // PyGC_Head #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() @@ -181,4 +183,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=f52d38b984314a55 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/enumobject.c.h b/Objects/clinic/enumobject.c.h index 1bda482f4955ae..a168e83c716a49 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, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -59,12 +60,11 @@ 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 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 +81,46 @@ 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 ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +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; + } + 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); + +exit: + return return_value; +} + PyDoc_STRVAR(reversed_new__doc__, "reversed(object, /)\n" "--\n" @@ -110,4 +150,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=e72fb89486919388 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/tupleobject.c.h b/Objects/clinic/tupleobject.c.h index 1c12706c0bb43b..1102f641bbd5a4 100644 --- a/Objects/clinic/tupleobject.c.h +++ b/Objects/clinic/tupleobject.c.h @@ -111,6 +111,31 @@ 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 (!_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 +152,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=56cf5ffc37c3e748 input=a9049054013a1b77]*/ diff --git a/Objects/enumobject.c b/Objects/enumobject.c index 364d508dd01822..3a2c3123784518 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 (%zd given)", nargs + nkwargs); - return NULL; -} - static void enum_dealloc(PyObject *op) { @@ -336,7 +272,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 ***************************************************************/ @@ -350,6 +286,7 @@ typedef struct { #define _reversedobject_CAST(op) ((reversedobject *)(op)) /*[clinic input] +@vectorcall @classmethod reversed.__new__ as reversed_new @@ -361,7 +298,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; @@ -403,22 +340,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) { diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 6120e70f3eeea4..745b8adc75d35d 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -781,6 +781,7 @@ static PyObject * tuple_subtype_new(PyTypeObject *type, PyObject *iterable); /*[clinic input] +@vectorcall @classmethod tuple.__new__ as tuple_new iterable: object(c_default="NULL") = () @@ -796,7 +797,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=8fdda913493ebe48]*/ { if (type != &PyTuple_Type) return tuple_subtype_new(type, iterable); @@ -809,27 +810,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) { diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index d7248c34c59be4..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'): (125_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), diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index db575d870be5c5..ae4bc8183c9643 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 - VcKwOnly_Type - +Modules/_testclinic.c - VcNew_Type - +Modules/_testclinic.c - VcNewExact_Type - ################################## diff --git a/Tools/clinic/libclinic/app.py b/Tools/clinic/libclinic/app.py index 9e8cec5320f877..a19a4d70a71492 100644 --- a/Tools/clinic/libclinic/app.py +++ b/Tools/clinic/libclinic/app.py @@ -120,7 +120,9 @@ 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'), 'methoddef_ifndef': d('file', 1), 'impl_definition': d('block'), diff --git a/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py index 7f02c7790f015a..fd2a86f0bda2a8 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 90e2e0d3d9c928..a75848951a2ce4 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,6 +302,7 @@ def reset(self) -> None: self.critical_section = False self.target_critical_section = [] self.disable_fastcall = False + self.vectorcall: VectorcallOptions | None = None self.permit_long_summary = False self.permit_long_docstring_body = False @@ -466,6 +467,17 @@ 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 is not None: + fail("Called @vectorcall twice!") + 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: if self.coexist: fail("Called @coexist twice!") @@ -599,6 +611,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 +739,8 @@ 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, ) self.add_function(func) diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index f981f0bcaf89f0..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,6 +116,7 @@ class Function: critical_section: bool = False disable_fastcall: bool = False target_critical_section: list[str] = dc.field(default_factory=list) + 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 bca87ecd75100c..3e73e6d550efc5 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_INIT) from libclinic.converter import CConverter from libclinic.converters import ( defining_class_converter, object_converter, self_converter) @@ -99,12 +99,13 @@ def declare_parser( NO_VARARG: Final[str] = "PY_SSIZE_T_MAX" PARSER_PROTOTYPE_KEYWORD: Final[str] = libclinic.normalize_snippet(""" - static PyObject * + static {return_type} {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs) """) -PARSER_PROTOTYPE_KEYWORD___INIT__: Final[str] = libclinic.normalize_snippet(""" - static int - {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs) +PARSER_PROTOTYPE_KEYWORD_HELPER: Final[str] = libclinic.normalize_snippet(""" + 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) """) PARSER_PROTOTYPE_VARARGS: Final[str] = libclinic.normalize_snippet(""" static PyObject * @@ -229,6 +230,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 @@ -346,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, @@ -612,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 = [] @@ -650,7 +656,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): @@ -692,10 +697,26 @@ 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" + 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) + self.declarations += "\nPyObject *argsbuf[%s];" % (len(self.converters) or 1) + self.declarations += "\nPyObject * const *fastargs;" + if has_optional_kw: + self.declarations += ( + "\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" - 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) @@ -797,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})) @@ -857,15 +878,34 @@ def copy_includes(self) -> None: def handle_new_or_init(self) -> None: self.methoddef_define = '' - if self.func.kind is METHOD_NEW: - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD - else: + if self.func.kind is METHOD_INIT: self.return_value_declaration = "int return_value = -1;" - self.parser_prototype = PARSER_PROTOTYPE_KEYWORD___INIT__ + entry_prototype = self._keyword_prototype(PARSER_PROTOTYPE_KEYWORD) + + 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 ? PyDict_GET_SIZE(kwargs) : 0,', + ' 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 @@ -936,6 +976,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: @@ -949,10 +992,12 @@ 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, "methoddef_ifndef" : self.methoddef_ifndef, + "vectorcall_definition" : self.vectorcall_definition, } # make sure we didn't forget to assign something, @@ -965,6 +1010,286 @@ 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_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) -> 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) + 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 or all_pos_only: + self.codegen.add_include('pycore_modsupport.h', + '_PyArg_NoKwnames()') + emit(""" + if (!_PyArg_NoKwnames("{name}", kwnames)) {{ + goto exit; + }} + """) + + if no_params: + emit(""" + if (nargs != 0) {{ + PyErr_Format(PyExc_TypeError, + "{name}() takes no arguments (%zd given)", + nargs); + goto exit; + }} + """) + return parser_code, True + + 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()') + emit(""" + if (!_PyArg_ParseStack(args, nargs, "{format_units}:{name}", + {parse_arguments})) {{ + goto exit; + }} + """) + return parser_code, True + parser_code.extend(pos_code) + 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; + }} + """) + + 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, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); + if (_result != 0) {{ + Py_DECREF(self); + return NULL; + }} + return self; + }} + """) + else: + emit(""" + return {c_basename}_parse_args(_PyType_CAST(type), args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); + """) + + 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() + return libclinic.normalize_snippet(f""" + static PyObject * + {vc_basename}(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) + """) + + 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; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + {declarations} + {initializers} + """) + "\n" + + 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} + {{ + PyObject *self = _PyType_CAST(type)->tp_alloc( + _PyType_CAST(type), 0); + if (self == NULL) {{ + goto exit; + }} + int _result; + {lock} + _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 + return libclinic.normalize_snippet(""" + {modifications} + {lock} + return_value = {c_basename}_impl({vc_impl_arguments}); + {unlock} + {return_conversion} + {post_parsing} + + {exit_label} + {cleanup} + return return_value; + }} + """) + + 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(needs_finale)] + + exact_check = self._vc_exact_check() + if exact_check: + lines.append(exact_check) + + lines.extend(parsing_code) + 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), + parser_declarations='') + return code + def parse_args(self, clang: CLanguage) -> dict[str, str]: self.select_prototypes() self.init_limited_capi() @@ -973,8 +1298,10 @@ 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 = "" # parser_body_fields remembers the fields passed in to the # previous call to parser_body. this is used for an awful hack. @@ -1000,4 +1327,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()