From 78c9c5481298819fb36b956d0949b455732bf074 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Thu, 14 Aug 2025 03:17:40 +0000 Subject: [PATCH 01/12] [mypyc] feat: `__mypyc_empty_tuple__` constant --- mypyc/irbuild/ll_builder.py | 8 ++++++-- mypyc/lib-rt/CPy.h | 7 +++++++ mypyc/lib-rt/init.c | 10 ++++++++++ mypyc/lib-rt/tuple_ops.c | 3 +++ mypyc/primitives/tuple_ops.py | 7 +++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 116a1bb4bae0..8f0d11b20cb6 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -188,6 +188,7 @@ ) from mypyc.primitives.tuple_ops import ( list_tuple_op, + load_empty_tuple_constant_op, new_tuple_op, new_tuple_with_length_op, sequence_tuple_op, @@ -2359,8 +2360,11 @@ def builtin_len(self, val: Value, line: int, use_pyssize_t: bool = False) -> Val return self.call_c(generic_len_op, [val], line) def new_tuple(self, items: list[Value], line: int) -> Value: - size: Value = Integer(len(items), c_pyssize_t_rprimitive) - return self.call_c(new_tuple_op, [size] + items, line) + if items: + size: Value = Integer(len(items), c_pyssize_t_rprimitive) + return self.call_c(new_tuple_op, [size] + items, line) + else: + return self.call_c(load_empty_tuple_constant_op, [], line) def new_tuple_with_length(self, length: Value, line: int) -> Value: """This function returns an uninitialized tuple. diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 8cd141545bbb..c51cd3117c32 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -64,6 +64,13 @@ typedef struct tuple_T4CIOO { } tuple_T4CIOO; #endif +// System-wide empty tuple constant +extern PyObject * __mypyc_empty_tuple__; + +static inline PyObject *_CPyTuple_LoadEmptyTupleConstant() { + Py_INCREF(__mypyc_empty_tuple__); + return __mypyc_empty_tuple__; +} // Native object operations diff --git a/mypyc/lib-rt/init.c b/mypyc/lib-rt/init.c index 01b133233489..653b224ece9d 100644 --- a/mypyc/lib-rt/init.c +++ b/mypyc/lib-rt/init.c @@ -10,4 +10,14 @@ PyObject *_CPy_ExcDummy = (PyObject *)&_CPy_ExcDummyStruct; // things at load time. void CPy_Init(void) { _CPy_ExcDummyStruct.ob_base.ob_type = &PyBaseObject_Type; + + // Initialize system-wide empty tuple constant + if (__mypyc_empty_tuple__ == NULL) { + __mypyc_empty_tuple__ = PyTuple_New(0); + if (!__mypyc_empty_tuple__) { + PyErr_SetString(PyExc_RuntimeError, "Failed to initialize __mypyc_empty_tuple__"); + return; + } + Py_INCREF(__mypyc_empty_tuple__); + } } diff --git a/mypyc/lib-rt/tuple_ops.c b/mypyc/lib-rt/tuple_ops.c index 1df73f1907e2..1c0d694ae4bb 100644 --- a/mypyc/lib-rt/tuple_ops.c +++ b/mypyc/lib-rt/tuple_ops.c @@ -5,6 +5,9 @@ #include #include "CPy.h" +// System-wide empty tuple constant +PyObject * __mypyc_empty_tuple__ = NULL; + PyObject *CPySequenceTuple_GetItem(PyObject *tuple, CPyTagged index) { if (CPyTagged_CheckShort(index)) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); diff --git a/mypyc/primitives/tuple_ops.py b/mypyc/primitives/tuple_ops.py index f262dec8b05a..e8da5b503624 100644 --- a/mypyc/primitives/tuple_ops.py +++ b/mypyc/primitives/tuple_ops.py @@ -55,6 +55,13 @@ error_kind=ERR_MAGIC, ) +load_empty_tuple_constant_op = custom_op( + arg_types=[], + return_type=tuple_rprimitive, + c_function_name="_CPyTuple_LoadEmptyTupleConstant", + error_kind=ERR_NEVER, +) + # PyTuple_SET_ITEM does no error checking, # and should only be used to fill in brand new tuples. new_tuple_set_item_op = custom_op( From ed00a0a4a807703b33ea93c66abad922197e790b Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:44:22 -0400 Subject: [PATCH 02/12] Update irbuild-basic.test --- mypyc/test-data/irbuild-basic.test | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index 63e4ef55d3fc..67f5758acd71 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -1755,11 +1755,13 @@ L0: r7 = __main__.globals :: static r8 = 'f' r9 = CPyDict_GetItem(r7, r8) - r10 = PyTuple_Pack(0) - r11 = PyDict_Copy(r6) - r12 = PyObject_Call(r9, r10, r11) - r13 = unbox(tuple[int, int, int], r12) - return r13 + r10 = PyDict_New() + r11 = CPyDict_UpdateInDisplay(r10, r6) + r12 = r11 >= 0 :: signed + r13 = _CPyTuple_LoadEmptyTupleConstant() + r14 = PyObject_Call(r9, r13, r10) + r15 = unbox(tuple[int, int, int], r14) + return r15 def h(): r0, r1 :: str r2, r3 :: object From 30eaf791826446c195f9a68a6a729cf9bc961494 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:44:43 -0400 Subject: [PATCH 03/12] Update irbuild-classes.test --- mypyc/test-data/irbuild-classes.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 68bc18c7bdeb..5a983bc134d3 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -297,7 +297,7 @@ L2: r27 = CPyType_FromTemplate(r26, r24, r25) r28 = C_trait_vtable_setup() r29 = '__mypyc_attrs__' - r30 = PyTuple_Pack(0) + r30 = _CPyTuple_LoadEmptyTupleConstant() r31 = PyObject_SetAttr(r27, r29, r30) r32 = r31 >= 0 :: signed __main__.C = r27 :: type @@ -310,7 +310,7 @@ L2: r39 = __main__.S_template :: type r40 = CPyType_FromTemplate(r39, r37, r38) r41 = '__mypyc_attrs__' - r42 = PyTuple_Pack(0) + r42 = _CPyTuple_LoadEmptyTupleConstant() r43 = PyObject_SetAttr(r40, r41, r42) r44 = r43 >= 0 :: signed __main__.S = r40 :: type From e8589908d42bd7057db9dd592a77186d8b2ba7b7 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:50:23 -0400 Subject: [PATCH 04/12] Update tuple_ops.c --- mypyc/lib-rt/tuple_ops.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/mypyc/lib-rt/tuple_ops.c b/mypyc/lib-rt/tuple_ops.c index 1c0d694ae4bb..1df73f1907e2 100644 --- a/mypyc/lib-rt/tuple_ops.c +++ b/mypyc/lib-rt/tuple_ops.c @@ -5,9 +5,6 @@ #include #include "CPy.h" -// System-wide empty tuple constant -PyObject * __mypyc_empty_tuple__ = NULL; - PyObject *CPySequenceTuple_GetItem(PyObject *tuple, CPyTagged index) { if (CPyTagged_CheckShort(index)) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); From c417c5568819f8aeca70a14558fca8862ef701ad Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:50:49 -0400 Subject: [PATCH 05/12] Update init.c --- mypyc/lib-rt/init.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypyc/lib-rt/init.c b/mypyc/lib-rt/init.c index 653b224ece9d..74137e13a980 100644 --- a/mypyc/lib-rt/init.c +++ b/mypyc/lib-rt/init.c @@ -4,6 +4,9 @@ struct ExcDummyStruct _CPy_ExcDummyStruct = { PyObject_HEAD_INIT(NULL) }; PyObject *_CPy_ExcDummy = (PyObject *)&_CPy_ExcDummyStruct; +// System-wide empty tuple constant +PyObject * __mypyc_empty_tuple__ = NULL; + // Because its dynamic linker is more restricted than linux/OS X, // Windows doesn't allow initializing globals with values from // other dynamic libraries. This means we need to initialize From fecc89c59c4953961805bdd6639c8cf4982e1fb5 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:41:40 -0400 Subject: [PATCH 06/12] no refcounting --- mypyc/lib-rt/CPy.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index c51cd3117c32..dfa4381ee4b0 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -68,7 +68,8 @@ typedef struct tuple_T4CIOO { extern PyObject * __mypyc_empty_tuple__; static inline PyObject *_CPyTuple_LoadEmptyTupleConstant() { - Py_INCREF(__mypyc_empty_tuple__); + // do tests still pass if I comment this out? empty tuple singleton is not tracked by gc + // Py_INCREF(__mypyc_empty_tuple__); return __mypyc_empty_tuple__; } From 11f89bdd3ef313104cec5e9aba949f17a3feea8b Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Mon, 25 Aug 2025 14:07:18 +0000 Subject: [PATCH 07/12] feat: adapt ir to latest changes on master --- mypyc/test-data/irbuild-basic.test | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index 67f5758acd71..842fa6e543bc 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -1755,13 +1755,11 @@ L0: r7 = __main__.globals :: static r8 = 'f' r9 = CPyDict_GetItem(r7, r8) - r10 = PyDict_New() - r11 = CPyDict_UpdateInDisplay(r10, r6) - r12 = r11 >= 0 :: signed - r13 = _CPyTuple_LoadEmptyTupleConstant() - r14 = PyObject_Call(r9, r13, r10) - r15 = unbox(tuple[int, int, int], r14) - return r15 + r10 = _CPyTuple_LoadEmptyTupleConstant() + r11 = PyDict_Copy(r6) + r12 = PyObject_Call(r9, r10, r11) + r13 = unbox(tuple[int, int, int], r12) + return r13 def h(): r0, r1 :: str r2, r3 :: object From 7473417dc9cdd6bd6a5438ba1dba5db3a4f76d72 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:08:46 -0400 Subject: [PATCH 08/12] Update init.c --- mypyc/lib-rt/init.c | 1 - 1 file changed, 1 deletion(-) diff --git a/mypyc/lib-rt/init.c b/mypyc/lib-rt/init.c index 74137e13a980..bd43e9d8650a 100644 --- a/mypyc/lib-rt/init.c +++ b/mypyc/lib-rt/init.c @@ -21,6 +21,5 @@ void CPy_Init(void) { PyErr_SetString(PyExc_RuntimeError, "Failed to initialize __mypyc_empty_tuple__"); return; } - Py_INCREF(__mypyc_empty_tuple__); } } From ea6dab00c3ac1fa3b798df61ccccc1ea9e729ac2 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Tue, 26 Aug 2025 18:12:50 +0000 Subject: [PATCH 09/12] rename _CPyTuple_LoadEmptyTupleConstant --- mypyc/lib-rt/CPy.h | 2 +- mypyc/primitives/tuple_ops.py | 2 +- mypyc/test-data/irbuild-basic.test | 2 +- mypyc/test-data/irbuild-classes.test | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index dfa4381ee4b0..3f93fe74f0cb 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -67,7 +67,7 @@ typedef struct tuple_T4CIOO { // System-wide empty tuple constant extern PyObject * __mypyc_empty_tuple__; -static inline PyObject *_CPyTuple_LoadEmptyTupleConstant() { +static inline PyObject *CPyTuple_LoadEmptyTupleConstant() { // do tests still pass if I comment this out? empty tuple singleton is not tracked by gc // Py_INCREF(__mypyc_empty_tuple__); return __mypyc_empty_tuple__; diff --git a/mypyc/primitives/tuple_ops.py b/mypyc/primitives/tuple_ops.py index e8da5b503624..ab23f8c441f5 100644 --- a/mypyc/primitives/tuple_ops.py +++ b/mypyc/primitives/tuple_ops.py @@ -58,7 +58,7 @@ load_empty_tuple_constant_op = custom_op( arg_types=[], return_type=tuple_rprimitive, - c_function_name="_CPyTuple_LoadEmptyTupleConstant", + c_function_name="CPyTuple_LoadEmptyTupleConstant", error_kind=ERR_NEVER, ) diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index 842fa6e543bc..ea48b3cc2e01 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -1755,7 +1755,7 @@ L0: r7 = __main__.globals :: static r8 = 'f' r9 = CPyDict_GetItem(r7, r8) - r10 = _CPyTuple_LoadEmptyTupleConstant() + r10 = CPyTuple_LoadEmptyTupleConstant() r11 = PyDict_Copy(r6) r12 = PyObject_Call(r9, r10, r11) r13 = unbox(tuple[int, int, int], r12) diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 5a983bc134d3..cd39a8b1c0c4 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -297,7 +297,7 @@ L2: r27 = CPyType_FromTemplate(r26, r24, r25) r28 = C_trait_vtable_setup() r29 = '__mypyc_attrs__' - r30 = _CPyTuple_LoadEmptyTupleConstant() + r30 = CPyTuple_LoadEmptyTupleConstant() r31 = PyObject_SetAttr(r27, r29, r30) r32 = r31 >= 0 :: signed __main__.C = r27 :: type @@ -310,7 +310,7 @@ L2: r39 = __main__.S_template :: type r40 = CPyType_FromTemplate(r39, r37, r38) r41 = '__mypyc_attrs__' - r42 = _CPyTuple_LoadEmptyTupleConstant() + r42 = CPyTuple_LoadEmptyTupleConstant() r43 = PyObject_SetAttr(r40, r41, r42) r44 = r43 >= 0 :: signed __main__.S = r40 :: type From 3824716692fe606c514f1e5d6678fa977b1a628e Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Tue, 26 Aug 2025 19:19:56 +0000 Subject: [PATCH 10/12] track reference count on python < 3.12 --- mypyc/lib-rt/CPy.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 3f93fe74f0cb..b4d3a0013ae7 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -68,8 +68,9 @@ typedef struct tuple_T4CIOO { extern PyObject * __mypyc_empty_tuple__; static inline PyObject *CPyTuple_LoadEmptyTupleConstant() { - // do tests still pass if I comment this out? empty tuple singleton is not tracked by gc - // Py_INCREF(__mypyc_empty_tuple__); +#if !CPY_3_12_FEATURES + Py_INCREF(__mypyc_empty_tuple__); +#endif return __mypyc_empty_tuple__; } From d29dfb97f1dffc683ea3fb52717e27554ab925fd Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Tue, 26 Aug 2025 19:30:53 +0000 Subject: [PATCH 11/12] CPyError_OutOfMemory --- mypyc/lib-rt/init.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypyc/lib-rt/init.c b/mypyc/lib-rt/init.c index bd43e9d8650a..9215c2d59019 100644 --- a/mypyc/lib-rt/init.c +++ b/mypyc/lib-rt/init.c @@ -18,8 +18,7 @@ void CPy_Init(void) { if (__mypyc_empty_tuple__ == NULL) { __mypyc_empty_tuple__ = PyTuple_New(0); if (!__mypyc_empty_tuple__) { - PyErr_SetString(PyExc_RuntimeError, "Failed to initialize __mypyc_empty_tuple__"); - return; + CPyError_OutOfMemory(); } } } From 18c4b5739ebb9a0eb616188fe398f4e42c12c3e3 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Wed, 27 Aug 2025 10:43:15 -0400 Subject: [PATCH 12/12] box/unbox empty tuple --- mypyc/codegen/emit.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 9ca761bd8ac5..4ef53296ef0d 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -1036,17 +1036,21 @@ def emit_box( self.emit_line(f"{declaration}{dest} = PyFloat_FromDouble({src});") elif isinstance(typ, RTuple): self.declare_tuple_struct(typ) - self.emit_line(f"{declaration}{dest} = PyTuple_New({len(typ.types)});") - self.emit_line(f"if (unlikely({dest} == NULL))") - self.emit_line(" CPyError_OutOfMemory();") - # TODO: Fail if dest is None - for i in range(len(typ.types)): - if not typ.is_unboxed: - self.emit_line(f"PyTuple_SET_ITEM({dest}, {i}, {src}.f{i}") - else: - inner_name = self.temp_name() - self.emit_box(f"{src}.f{i}", inner_name, typ.types[i], declare_dest=True) - self.emit_line(f"PyTuple_SET_ITEM({dest}, {i}, {inner_name});") + if not typ.types: + self.emit_line(f"{declaration}{dest} = CPyTuple_LoadEmptyTupleConstant();") + else: + self.emit_line(f"{declaration}{dest} = PyTuple_New({len(typ.types)});") + self.emit_line(f"if (unlikely({dest} == NULL))") + self.emit_line(" CPyError_OutOfMemory();") + + # TODO: Fail if dest is None + for i in range(len(typ.types)): + if not typ.is_unboxed: + self.emit_line(f"PyTuple_SET_ITEM({dest}, {i}, {src}.f{i}") + else: + inner_name = self.temp_name() + self.emit_box(f"{src}.f{i}", inner_name, typ.types[i], declare_dest=True) + self.emit_line(f"PyTuple_SET_ITEM({dest}, {i}, {inner_name});") else: assert not typ.is_unboxed # Type is boxed -- trivially just assign.