Skip to content
26 changes: 15 additions & 11 deletions mypyc/codegen/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions mypyc/irbuild/ll_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also use the new primitive in new_tuple_with_length below if length == 0.

Copy link
Contributor Author

@BobTheBuidler BobTheBuidler Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to implement this logic at compile time because in this location, length is a Value not a known integer

we could do a boolean op to check, but then we're just recreating what already exists in PyTuple_New and making our IR longer to do it

Copy link
Collaborator

@JukkaL JukkaL Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, please disregard my above comment.

The new function could be used in mypyc/codegen/emit.py on this line when generating C for a box operation: self.emit_line(f"{declaration}{dest} = PyTuple_New({len(typ.types)});"). If length is zero, we could use the new primitive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks correct. We can also skip the subsequent NULL check. I've implemented this and the tests are running, I may need to update the IR test definitons.


def new_tuple_with_length(self, length: Value, line: int) -> Value:
"""This function returns an uninitialized tuple.
Expand Down
9 changes: 9 additions & 0 deletions mypyc/lib-rt/CPy.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ typedef struct tuple_T4CIOO {
} tuple_T4CIOO;
#endif

// System-wide empty tuple constant
extern PyObject * __mypyc_empty_tuple__;

static inline PyObject *CPyTuple_LoadEmptyTupleConstant() {
#if !CPY_3_12_FEATURES
Py_INCREF(__mypyc_empty_tuple__);
#endif
return __mypyc_empty_tuple__;
}

// Native object operations

Expand Down
11 changes: 11 additions & 0 deletions mypyc/lib-rt/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@
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
// 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__) {
CPyError_OutOfMemory();
}
}
}
7 changes: 7 additions & 0 deletions mypyc/primitives/tuple_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion mypyc/test-data/irbuild-basic.test
Original file line number Diff line number Diff line change
Expand Up @@ -1755,7 +1755,7 @@ L0:
r7 = __main__.globals :: static
r8 = 'f'
r9 = CPyDict_GetItem(r7, r8)
r10 = PyTuple_Pack(0)
r10 = CPyTuple_LoadEmptyTupleConstant()
r11 = PyDict_Copy(r6)
r12 = PyObject_Call(r9, r10, r11)
r13 = unbox(tuple[int, int, int], r12)
Expand Down
4 changes: 2 additions & 2 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading