diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index df473d59fff3d8e..959732fc6e4a83d 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -63,6 +63,30 @@ def test_argument_handling(self): self.assertRaises(SyntaxError, exec, 'def f(a = 0, a = 1): pass') self.assertRaises(SyntaxError, exec, 'def f(a): global a; a = 1') + def test_call_opcode_stack_use_limit(self): + def get_call_opcode(positional_count, keyword_count): + args = ["0"] * positional_count + args.extend(f"a{i}=0" for i in range(keyword_count)) + code = compile(f"f({', '.join(args)})", "", "exec") + return next( + instr.opname for instr in dis.get_instructions(code) + if instr.opname.startswith("CALL") + ) + + for positional_count, keyword_count, expected_opcode in [ + (0, 16, "CALL_KW"), + (15, 14, "CALL_KW"), + (15, 15, "CALL_FUNCTION_EX"), + ]: + with self.subTest( + positional_count=positional_count, + keyword_count=keyword_count, + ): + self.assertEqual( + get_call_opcode(positional_count, keyword_count), + expected_opcode, + ) + def test_syntax_error(self): self.assertRaises(SyntaxError, compile, "1+*3", "filename", "exec") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-14-19-00-00.gh-issue-155141.call-kw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-14-19-00-00.gh-issue-155141.call-kw.rst new file mode 100644 index 000000000000000..caee6e10168eabd --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-14-19-00-00.gh-issue-155141.call-kw.rst @@ -0,0 +1,3 @@ +Compile pure-keyword calls with 16 to 29 keyword arguments using the faster +``CALL_KW`` instruction. This includes common cases such as dataclass +constructors with many fields. diff --git a/Python/codegen.c b/Python/codegen.c index bedf3b17c52ce44..ffff88f1c99ba0a 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -87,6 +87,9 @@ typedef _PyCompile_FBlockInfo fblockinfo; #define LOC(x) SRC_LOCATION_FROM_AST(x) +#define CALL_STACK_USE(nargs, nkwds) \ + ((nargs) + (nkwds) + ((nkwds) != 0)) + #define NEW_JUMP_TARGET_LABEL(C, NAME) \ jump_target_label NAME = _PyInstructionSequence_NewLabel(INSTR_SEQUENCE(C)); \ if (!IS_JUMP_TARGET_LABEL(NAME)) { \ @@ -4134,7 +4137,7 @@ maybe_optimize_method_call(compiler *c, expr_ty e) /* Check that there aren't too many arguments */ argsl = asdl_seq_LEN(args); kwdsl = asdl_seq_LEN(kwds); - if (argsl + kwdsl + (kwdsl != 0) >= _PY_STACK_USE_GUIDELINE) { + if (CALL_STACK_USE(argsl, kwdsl) >= _PY_STACK_USE_GUIDELINE) { return 0; } /* Check that there are no *varargs types of arguments. */ @@ -4427,7 +4430,7 @@ codegen_call_helper_impl(compiler *c, location loc, nelts = asdl_seq_LEN(args); nkwelts = asdl_seq_LEN(keywords); - if (nelts + nkwelts*2 > _PY_STACK_USE_GUIDELINE) { + if (CALL_STACK_USE(nelts, nkwelts) > _PY_STACK_USE_GUIDELINE) { goto ex_call; } for (i = 0; i < nelts; i++) {