From 67742d92c411a93e7692f761ffeb3af75ba1ff87 Mon Sep 17 00:00:00 2001 From: Peter Gessler Date: Sat, 30 May 2026 17:09:02 -0500 Subject: [PATCH 1/7] Optimize leading empty tuple unpack idioms in codegen Includes the branch-local test expectations for the leading-only behavior. --- Lib/test/test_compile.py | 133 +++++++++++++++++++++++++++++++++++++++ Python/codegen.c | 29 ++++++--- 2 files changed, 155 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 9edbca3c383b43d..911f79a38feb564 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1119,6 +1119,139 @@ def or_false(x): self.assertIn('LOAD_', opcodes[-2].opname) self.assertEqual('RETURN_VALUE', opcodes[-1].opname) + def test_empty_set_unpack_literal_bytecode_optimization(self): + cases = { + # optimized cases + '{*()}': [ + ('RESUME', 0), + ('BUILD_SET', 0), + ('RETURN_VALUE', None), + ], + '{*(), 1}': [ + ('RESUME', 0), + ('LOAD_SMALL_INT', 1), + ('BUILD_SET', 1), + ('RETURN_VALUE', None), + ], + '{*(), 1, 2, 3}': [ + ('RESUME', 0), + ('BUILD_SET', 0), + ('LOAD_CONST', frozenset({1, 2, 3})), + ('SET_UPDATE', 1), + ('RETURN_VALUE', None), + ], + # unoptimized cases + '{*(1,)}': [ + ('RESUME', 0), + ('BUILD_SET', 0), + ('LOAD_CONST', (1,)), + ('SET_UPDATE', 1), + ('RETURN_VALUE', None), + ], + '{*(x,)}': [ + ('RESUME', 0), + ('BUILD_SET', 0), + ('LOAD_NAME', 'x'), + ('BUILD_TUPLE', 1), + ('SET_UPDATE', 1), + ('RETURN_VALUE', None), + ], + '{1, *()}': [ + ('RESUME', 0), + ('LOAD_SMALL_INT', 1), + ('BUILD_SET', 1), + ('LOAD_COMMON_CONSTANT', ()), + ('SET_UPDATE', 1), + ('RETURN_VALUE', None), + ], + '{1, 2, 3, *()}': [ + ('RESUME', 0), + ('BUILD_SET', 0), + ('LOAD_CONST', frozenset({1, 2, 3})), + ('SET_UPDATE', 1), + ('LOAD_COMMON_CONSTANT', ()), + ('SET_UPDATE', 1), + ('RETURN_VALUE', None), + ], + } + + for source, expected in cases.items(): + with self.subTest(source=source): + code = compile(source, '', 'eval') + instructions = [ + (instruction.opname, instruction.argval) + for instruction in dis.get_instructions(code) + ] + self.assertEqual(instructions, expected) + + def test_empty_leading_tuple_unpack_list_and_tuple_bytecode_optimization(self): + cases = { + # optimized cases + '[*()]': [ + ('RESUME', 0), + ('BUILD_LIST', 0), + ('RETURN_VALUE', None), + ], + '[*(), 1]': [ + ('RESUME', 0), + ('LOAD_SMALL_INT', 1), + ('BUILD_LIST', 1), + ('RETURN_VALUE', None), + ], + '(*(),)': [ + ('RESUME', 0), + ('LOAD_COMMON_CONSTANT', ()), + ('RETURN_VALUE', None), + ], + '(*(), 1)': [ + ('RESUME', 0), + ('LOAD_CONST', (1,)), + ('RETURN_VALUE', None), + ], + # unoptimized cases + '[*(1,)]': [ + ('RESUME', 0), + ('BUILD_LIST', 0), + ('LOAD_CONST', (1,)), + ('LIST_EXTEND', 1), + ('RETURN_VALUE', None), + ], + '[*(x,)]': [ + ('RESUME', 0), + ('BUILD_LIST', 0), + ('LOAD_NAME', 'x'), + ('BUILD_TUPLE', 1), + ('LIST_EXTEND', 1), + ('RETURN_VALUE', None), + ], + '[1, *()]': [ + ('RESUME', 0), + ('LOAD_SMALL_INT', 1), + ('BUILD_LIST', 1), + ('LOAD_COMMON_CONSTANT', ()), + ('LIST_EXTEND', 1), + ('RETURN_VALUE', None), + ], + '[1, 2, 3, *()]': [ + ('RESUME', 0), + ('BUILD_LIST', 0), + ('LOAD_CONST', (1, 2, 3)), + ('LIST_EXTEND', 1), + ('LOAD_COMMON_CONSTANT', ()), + ('LIST_EXTEND', 1), + ('RETURN_VALUE', None), + ], + } + + for source, expected in cases.items(): + with self.subTest(source=source): + code = compile(source, '', 'eval') + instructions = [ + (instruction.opname, instruction.argval) + for instruction in dis.get_instructions(code) + ] + self.assertEqual(instructions, expected) + def test_imported_load_method(self): sources = [ """\ diff --git a/Python/codegen.c b/Python/codegen.c index 205c49cff1827c4..7efb7623958f539 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -3414,13 +3414,15 @@ codegen_boolop(compiler *c, expr_ty e) static int starunpack_helper_impl(compiler *c, location loc, - asdl_expr_seq *elts, PyObject *injected_arg, int pushed, + asdl_expr_seq *elts, Py_ssize_t start, + PyObject *injected_arg, int pushed, int build, int add, int extend, int tuple) { - Py_ssize_t n = asdl_seq_LEN(elts); + Py_ssize_t end = asdl_seq_LEN(elts); + Py_ssize_t n = end - start; int big = n + pushed + (injected_arg ? 1 : 0) > _PY_STACK_USE_GUIDELINE; int seen_star = 0; - for (Py_ssize_t i = 0; i < n; i++) { + for (Py_ssize_t i = start; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); if (elt->kind == Starred_kind) { seen_star = 1; @@ -3428,7 +3430,7 @@ starunpack_helper_impl(compiler *c, location loc, } } if (!seen_star && !big) { - for (Py_ssize_t i = 0; i < n; i++) { + for (Py_ssize_t i = start; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); VISIT(c, expr, elt); } @@ -3448,7 +3450,7 @@ starunpack_helper_impl(compiler *c, location loc, ADDOP_I(c, loc, build, pushed); sequence_built = 1; } - for (Py_ssize_t i = 0; i < n; i++) { + for (Py_ssize_t i = start; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); if (elt->kind == Starred_kind) { if (sequence_built == 0) { @@ -3476,12 +3478,25 @@ starunpack_helper_impl(compiler *c, location loc, return SUCCESS; } +static bool +is_empty_starred_tuple(expr_ty elt) +{ + if (elt->kind != Starred_kind) { + return false; + } + expr_ty value = elt->v.Starred.value; + return value->kind == Tuple_kind && + value->v.Tuple.ctx == Load && + asdl_seq_LEN(value->v.Tuple.elts) == 0; +} + static int starunpack_helper(compiler *c, location loc, asdl_expr_seq *elts, int pushed, int build, int add, int extend, int tuple) { - return starunpack_helper_impl(c, loc, elts, NULL, pushed, + Py_ssize_t start = asdl_seq_LEN(elts) && is_empty_starred_tuple(asdl_seq_GET(elts, 0)); + return starunpack_helper_impl(c, loc, elts, start, NULL, pushed, build, add, extend, tuple); } @@ -4446,7 +4461,7 @@ codegen_call_helper_impl(compiler *c, location loc, VISIT(c, expr, ((expr_ty)asdl_seq_GET(args, 0))->v.Starred.value); } else { - RETURN_IF_ERROR(starunpack_helper_impl(c, loc, args, injected_arg, n, + RETURN_IF_ERROR(starunpack_helper_impl(c, loc, args, 0, injected_arg, n, BUILD_LIST, LIST_APPEND, LIST_EXTEND, 1)); } /* Then keyword arguments */ From 0baf56469b4eb8d9b790021ee7974bc77d7469d2 Mon Sep 17 00:00:00 2001 From: Peter Gessler Date: Tue, 2 Jun 2026 16:21:23 -0500 Subject: [PATCH 2/7] add blurb --- .../2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst new file mode 100644 index 000000000000000..49b54af648ee998 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst @@ -0,0 +1,2 @@ +Optimize bytecode for leading null unpack cases such as the ``ast.unparse`` +empty set representation. From f7e81500df6e2272be9b6acf641096f162dc90bd Mon Sep 17 00:00:00 2001 From: Peter Gessler Date: Tue, 2 Jun 2026 17:15:18 -0500 Subject: [PATCH 3/7] rerun ci From a0849b093739172d844284efa9e6607c4a74779c Mon Sep 17 00:00:00 2001 From: Peter Gessler Date: Fri, 18 Sep 2026 18:47:45 -0500 Subject: [PATCH 4/7] omit all null unpack idioms --- Lib/test/test_compile.py | 77 +++++++++++++++++++++++++--------------- Python/codegen.c | 57 ++++++++++++++++------------- 2 files changed, 82 insertions(+), 52 deletions(-) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 911f79a38feb564..7608d147ed20c69 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1127,6 +1127,11 @@ def test_empty_set_unpack_literal_bytecode_optimization(self): ('BUILD_SET', 0), ('RETURN_VALUE', None), ], + '{*(), *()}': [ + ('RESUME', 0), + ('BUILD_SET', 0), + ('RETURN_VALUE', None), + ], '{*(), 1}': [ ('RESUME', 0), ('LOAD_SMALL_INT', 1), @@ -1140,36 +1145,39 @@ def test_empty_set_unpack_literal_bytecode_optimization(self): ('SET_UPDATE', 1), ('RETURN_VALUE', None), ], - # unoptimized cases - '{*(1,)}': [ + '{1, *()}': [ + ('RESUME', 0), + ('LOAD_SMALL_INT', 1), + ('BUILD_SET', 1), + ('RETURN_VALUE', None), + ], + '{1, 2, 3, *()}': [ ('RESUME', 0), ('BUILD_SET', 0), - ('LOAD_CONST', (1,)), + ('LOAD_CONST', frozenset({1, 2, 3})), ('SET_UPDATE', 1), ('RETURN_VALUE', None), ], - '{*(x,)}': [ + '{1, 2, *(), 3}': [ ('RESUME', 0), ('BUILD_SET', 0), - ('LOAD_NAME', 'x'), - ('BUILD_TUPLE', 1), + ('LOAD_CONST', frozenset({1, 2, 3})), ('SET_UPDATE', 1), ('RETURN_VALUE', None), ], - '{1, *()}': [ + # unoptimized cases + '{*(1,)}': [ ('RESUME', 0), - ('LOAD_SMALL_INT', 1), - ('BUILD_SET', 1), - ('LOAD_COMMON_CONSTANT', ()), + ('BUILD_SET', 0), + ('LOAD_CONST', (1,)), ('SET_UPDATE', 1), ('RETURN_VALUE', None), ], - '{1, 2, 3, *()}': [ + '{*(x,)}': [ ('RESUME', 0), ('BUILD_SET', 0), - ('LOAD_CONST', frozenset({1, 2, 3})), - ('SET_UPDATE', 1), - ('LOAD_COMMON_CONSTANT', ()), + ('LOAD_NAME', 'x'), + ('BUILD_TUPLE', 1), ('SET_UPDATE', 1), ('RETURN_VALUE', None), ], @@ -1192,6 +1200,11 @@ def test_empty_leading_tuple_unpack_list_and_tuple_bytecode_optimization(self): ('BUILD_LIST', 0), ('RETURN_VALUE', None), ], + '[*(), *()]': [ + ('RESUME', 0), + ('BUILD_LIST', 0), + ('RETURN_VALUE', None), + ], '[*(), 1]': [ ('RESUME', 0), ('LOAD_SMALL_INT', 1), @@ -1203,41 +1216,49 @@ def test_empty_leading_tuple_unpack_list_and_tuple_bytecode_optimization(self): ('LOAD_COMMON_CONSTANT', ()), ('RETURN_VALUE', None), ], + '(*(), *())': [ + ('RESUME', 0), + ('LOAD_COMMON_CONSTANT', ()), + ('RETURN_VALUE', None), + ], '(*(), 1)': [ ('RESUME', 0), ('LOAD_CONST', (1,)), ('RETURN_VALUE', None), ], - # unoptimized cases - '[*(1,)]': [ + '[1, *()]': [ + ('RESUME', 0), + ('LOAD_SMALL_INT', 1), + ('BUILD_LIST', 1), + ('RETURN_VALUE', None), + ], + '[1, 2, 3, *()]': [ ('RESUME', 0), ('BUILD_LIST', 0), - ('LOAD_CONST', (1,)), + ('LOAD_CONST', (1, 2, 3)), ('LIST_EXTEND', 1), ('RETURN_VALUE', None), ], - '[*(x,)]': [ + '[1, 2, *(), 3]': [ ('RESUME', 0), ('BUILD_LIST', 0), - ('LOAD_NAME', 'x'), - ('BUILD_TUPLE', 1), + ('LOAD_CONST', (1, 2, 3)), ('LIST_EXTEND', 1), ('RETURN_VALUE', None), ], - '[1, *()]': [ + # unoptimized cases + '[*(1,)]': [ ('RESUME', 0), - ('LOAD_SMALL_INT', 1), - ('BUILD_LIST', 1), - ('LOAD_COMMON_CONSTANT', ()), + ('BUILD_LIST', 0), + ('LOAD_CONST', (1,)), ('LIST_EXTEND', 1), ('RETURN_VALUE', None), ], - '[1, 2, 3, *()]': [ + '[*(x,)]': [ ('RESUME', 0), ('BUILD_LIST', 0), - ('LOAD_CONST', (1, 2, 3)), - ('LIST_EXTEND', 1), - ('LOAD_COMMON_CONSTANT', ()), + ('LOAD_NAME', 'x'), + ('BUILD_TUPLE', 1), ('LIST_EXTEND', 1), ('RETURN_VALUE', None), ], diff --git a/Python/codegen.c b/Python/codegen.c index 7efb7623958f539..89fdc4dd841e75c 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -3412,26 +3412,42 @@ codegen_boolop(compiler *c, expr_ty e) return SUCCESS; } +static bool +is_empty_starred_tuple(expr_ty elt) +{ + if (elt->kind != Starred_kind) { + return false; + } + expr_ty value = elt->v.Starred.value; + return value->kind == Tuple_kind && + asdl_seq_LEN(value->v.Tuple.elts) == 0; +} + static int starunpack_helper_impl(compiler *c, location loc, - asdl_expr_seq *elts, Py_ssize_t start, - PyObject *injected_arg, int pushed, + asdl_expr_seq *elts, PyObject *injected_arg, int pushed, int build, int add, int extend, int tuple) { Py_ssize_t end = asdl_seq_LEN(elts); - Py_ssize_t n = end - start; - int big = n + pushed + (injected_arg ? 1 : 0) > _PY_STACK_USE_GUIDELINE; + Py_ssize_t n = 0; int seen_star = 0; - for (Py_ssize_t i = start; i < end; i++) { + for (Py_ssize_t i = 0; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); if (elt->kind == Starred_kind) { + if (is_empty_starred_tuple(elt)) { + continue; + } seen_star = 1; - break; } + n++; } + int big = n + pushed + (injected_arg ? 1 : 0) > _PY_STACK_USE_GUIDELINE; if (!seen_star && !big) { - for (Py_ssize_t i = start; i < end; i++) { + for (Py_ssize_t i = 0; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); + if (is_empty_starred_tuple(elt)) { + continue; + } VISIT(c, expr, elt); } if (injected_arg) { @@ -3446,15 +3462,20 @@ starunpack_helper_impl(compiler *c, location loc, return SUCCESS; } int sequence_built = 0; + Py_ssize_t nitems = 0; if (big) { ADDOP_I(c, loc, build, pushed); sequence_built = 1; } - for (Py_ssize_t i = start; i < end; i++) { + for (Py_ssize_t i = 0; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); + if (elt->kind == Starred_kind) { + if (is_empty_starred_tuple(elt)) { + continue; + } if (sequence_built == 0) { - ADDOP_I(c, loc, build, i+pushed); + ADDOP_I(c, loc, build, nitems+pushed); sequence_built = 1; } VISIT(c, expr, elt->v.Starred.value); @@ -3466,6 +3487,7 @@ starunpack_helper_impl(compiler *c, location loc, ADDOP_I(c, loc, add, 1); } } + nitems++; } assert(sequence_built); if (injected_arg) { @@ -3478,25 +3500,12 @@ starunpack_helper_impl(compiler *c, location loc, return SUCCESS; } -static bool -is_empty_starred_tuple(expr_ty elt) -{ - if (elt->kind != Starred_kind) { - return false; - } - expr_ty value = elt->v.Starred.value; - return value->kind == Tuple_kind && - value->v.Tuple.ctx == Load && - asdl_seq_LEN(value->v.Tuple.elts) == 0; -} - static int starunpack_helper(compiler *c, location loc, asdl_expr_seq *elts, int pushed, int build, int add, int extend, int tuple) { - Py_ssize_t start = asdl_seq_LEN(elts) && is_empty_starred_tuple(asdl_seq_GET(elts, 0)); - return starunpack_helper_impl(c, loc, elts, start, NULL, pushed, + return starunpack_helper_impl(c, loc, elts, NULL, pushed, build, add, extend, tuple); } @@ -4461,7 +4470,7 @@ codegen_call_helper_impl(compiler *c, location loc, VISIT(c, expr, ((expr_ty)asdl_seq_GET(args, 0))->v.Starred.value); } else { - RETURN_IF_ERROR(starunpack_helper_impl(c, loc, args, 0, injected_arg, n, + RETURN_IF_ERROR(starunpack_helper_impl(c, loc, args, injected_arg, n, BUILD_LIST, LIST_APPEND, LIST_EXTEND, 1)); } /* Then keyword arguments */ From b05ab8940415b53e010dd92aea766c4274fd69ec Mon Sep 17 00:00:00 2001 From: Peter Gessler Date: Sun, 20 Sep 2026 22:07:42 -0500 Subject: [PATCH 5/7] add empty list/dict --- Python/codegen.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Python/codegen.c b/Python/codegen.c index 89fdc4dd841e75c..5855a8de31c1c14 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -3413,14 +3413,18 @@ codegen_boolop(compiler *c, expr_ty e) } static bool -is_empty_starred_tuple(expr_ty elt) +is_empty_starred_literal(expr_ty elt) { if (elt->kind != Starred_kind) { return false; } expr_ty value = elt->v.Starred.value; - return value->kind == Tuple_kind && - asdl_seq_LEN(value->v.Tuple.elts) == 0; + return (value->kind == Tuple_kind && + asdl_seq_LEN(value->v.Tuple.elts) == 0) || + (value->kind == List_kind && + asdl_seq_LEN(value->v.List.elts) == 0) || + (value->kind == Dict_kind && + asdl_seq_LEN(value->v.Dict.keys) == 0); } static int @@ -3434,7 +3438,7 @@ starunpack_helper_impl(compiler *c, location loc, for (Py_ssize_t i = 0; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); if (elt->kind == Starred_kind) { - if (is_empty_starred_tuple(elt)) { + if (is_empty_starred_literal(elt)) { continue; } seen_star = 1; @@ -3445,7 +3449,7 @@ starunpack_helper_impl(compiler *c, location loc, if (!seen_star && !big) { for (Py_ssize_t i = 0; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); - if (is_empty_starred_tuple(elt)) { + if (is_empty_starred_literal(elt)) { continue; } VISIT(c, expr, elt); @@ -3469,9 +3473,9 @@ starunpack_helper_impl(compiler *c, location loc, } for (Py_ssize_t i = 0; i < end; i++) { expr_ty elt = asdl_seq_GET(elts, i); - + if (elt->kind == Starred_kind) { - if (is_empty_starred_tuple(elt)) { + if (is_empty_starred_literal(elt)) { continue; } if (sequence_built == 0) { From e841f0c3513878fd5acc17d2957cbd96160759b1 Mon Sep 17 00:00:00 2001 From: Peter Gessler Date: Sun, 20 Sep 2026 22:14:28 -0500 Subject: [PATCH 6/7] update blurb --- .../2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst index 49b54af648ee998..b1895146df26cb8 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst @@ -1,2 +1,2 @@ -Optimize bytecode for leading null unpack cases such as the ``ast.unparse`` +Optimize bytecode for literal null unpack idiom cases such as the ``ast.unparse`` empty set representation. From 84257430d34c783463ea8e9a994cd49376fda92e Mon Sep 17 00:00:00 2001 From: Peter Gessler Date: Sun, 20 Sep 2026 22:29:33 -0500 Subject: [PATCH 7/7] Update 2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst Co-authored-by: Jelle Zijlstra --- .../2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst index b1895146df26cb8..35c2e562cf60835 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-02-16-21-02.gh-issue-150737.LoYUFY.rst @@ -1,2 +1 @@ -Optimize bytecode for literal null unpack idiom cases such as the ``ast.unparse`` -empty set representation. +Optimize bytecode for empty unpack cases such as ``{*()}``.