diff --git a/Include/internal/pycore_opcode_utils.h b/Include/internal/pycore_opcode_utils.h index 69178381993fba..7d3b7fe2bfeddc 100644 --- a/Include/internal/pycore_opcode_utils.h +++ b/Include/internal/pycore_opcode_utils.h @@ -75,7 +75,8 @@ extern "C" { #define CONSTANT_BUILTIN_ANY 4 #define CONSTANT_BUILTIN_LIST 5 #define CONSTANT_BUILTIN_SET 6 -#define NUM_COMMON_CONSTANTS 7 +#define CONSTANT_BUILTIN_FROZENDICT 7 +#define NUM_COMMON_CONSTANTS 8 /* Values used in the oparg for RESUME */ #define RESUME_AT_FUNC_START 0 diff --git a/Lib/opcode.py b/Lib/opcode.py index d53b94d89b46f7..81119b734df63b 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -41,7 +41,7 @@ _special_method_names = _opcode.get_special_method_names() _common_constants = [builtins.AssertionError, builtins.NotImplementedError, builtins.tuple, builtins.all, builtins.any, builtins.list, - builtins.set] + builtins.set, builtins.frozendict] _nb_ops = _opcode.get_nb_ops() hascompare = [opmap["COMPARE_OP"]] diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index e0cc010f15513b..1a5beecfdd04a9 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -241,6 +241,29 @@ def g(a): self.assertTrue(g(4)) self.check_lnotab(g) + def test_constant_folding_frozendict_call(self): + # frozendict(key=const, ...) should be folded to LOAD_CONST + # with a runtime guard (fallback CALL_KW path still exists) + def f(): + return frozendict(x=1, y=2) + + code = f.__code__ + self.assertInBytecode(code, 'LOAD_CONST', frozendict(x=1, y=2)) + self.assertInBytecode(code, 'LOAD_COMMON_CONSTANT', 7) + result = f() + self.assertEqual(result, frozendict(x=1, y=2)) + self.assertIsInstance(result, frozendict) + + def test_constant_folding_frozendict_call_shadowed(self): + # When frozendict is shadowed, the fallback path should be used + def f(): + frozendict = dict + return frozendict(x=1, y=2) + + result = f() + self.assertEqual(result, {'x': 1, 'y': 2}) + self.assertIsInstance(result, dict) + def test_constant_folding_small_int(self): tests = [ ('(0, )[0]', 0), diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-25-23-07-44.gh-issue-146381.t80WOO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-25-23-07-44.gh-issue-146381.t80WOO.rst new file mode 100644 index 00000000000000..e68555e2c24562 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-25-23-07-44.gh-issue-146381.t80WOO.rst @@ -0,0 +1,2 @@ +Fold frozendict(key=const, ...) calls to LOAD_CONST in codegen. Patch by +Donghee Na. diff --git a/Python/codegen.c b/Python/codegen.c index d300d77e0f73b0..7e0357285e6f19 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4128,6 +4128,66 @@ codegen_validate_keywords(compiler *c, asdl_keyword_seq *keywords) return SUCCESS; } +/* Try to fold frozendict(key=const, ...) into LOAD_CONST with a runtime guard. + Called after the function has been loaded onto the stack. + Return 1 if optimization was emitted, 0 if not, -1 on error. */ +static int +maybe_optimize_frozendict_call(compiler *c, expr_ty e, jump_target_label end) +{ + expr_ty func = e->v.Call.func; + asdl_expr_seq *args = e->v.Call.args; + asdl_keyword_seq *kwds = e->v.Call.keywords; + + if (func->kind != Name_kind || + !_PyUnicode_EqualToASCIIString(func->v.Name.id, "frozendict") || + asdl_seq_LEN(args) != 0) + { + return 0; + } + + /* All keywords must have names (no **kwargs) and constant values */ + Py_ssize_t nkwds = asdl_seq_LEN(kwds); + for (Py_ssize_t i = 0; i < nkwds; i++) { + keyword_ty kw = asdl_seq_GET(kwds, i); + if (kw->arg == NULL || kw->value->kind != Constant_kind) { + return 0; + } + } + + /* Build the frozendict at compile time */ + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return -1; + } + for (Py_ssize_t i = 0; i < nkwds; i++) { + keyword_ty kw = asdl_seq_GET(kwds, i); + if (PyDict_SetItem(dict, kw->arg, kw->value->v.Constant.value) < 0) { + Py_DECREF(dict); + return -1; + } + } + PyObject *fd = PyFrozenDict_New(dict); + Py_DECREF(dict); + if (fd == NULL) { + return -1; + } + + location loc = LOC(func); + NEW_JUMP_TARGET_LABEL(c, skip_optimization); + + ADDOP_I(c, loc, COPY, 1); + ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_BUILTIN_FROZENDICT); + ADDOP_COMPARE(c, loc, Is); + ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, skip_optimization); + ADDOP(c, loc, POP_TOP); + ADDOP_LOAD_CONST(c, LOC(e), fd); + Py_DECREF(fd); + ADDOP_JUMP(c, loc, JUMP, end); + + USE_LABEL(c, skip_optimization); + return 1; +} + static int codegen_call(compiler *c, expr_ty e) { @@ -4143,6 +4203,7 @@ codegen_call(compiler *c, expr_ty e) RETURN_IF_ERROR(check_caller(c, e->v.Call.func)); VISIT(c, expr, e->v.Call.func); RETURN_IF_ERROR(maybe_optimize_function_call(c, e, skip_normal_call)); + RETURN_IF_ERROR(maybe_optimize_frozendict_call(c, e, skip_normal_call)); location loc = LOC(e->v.Call.func); ADDOP(c, loc, PUSH_NULL); loc = LOC(e); diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 5da0f3e5be3a70..40a8f9a6128150 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -887,6 +887,7 @@ pycore_init_builtins(PyThreadState *tstate) interp->common_consts[CONSTANT_BUILTIN_ANY] = any; interp->common_consts[CONSTANT_BUILTIN_LIST] = (PyObject*)&PyList_Type; interp->common_consts[CONSTANT_BUILTIN_SET] = (PyObject*)&PySet_Type; + interp->common_consts[CONSTANT_BUILTIN_FROZENDICT] = (PyObject*)&PyFrozenDict_Type; for (int i=0; i < NUM_COMMON_CONSTANTS; i++) { assert(interp->common_consts[i] != NULL);