Repository: numba/numba

Issue:
Tuple unboxing needs to be stricter
```
>>> from numba import jit
>>> @jit(nopython=True)
   ....: def copy(x):
   ....:     return list(x)
   ....: 
>>> 
>>> copy([(1,), (2,3)])
[(1,), (2,)]
```



Discussion:

Resolving patch:
diff --git a/numba/pythonapi.py b/numba/pythonapi.py
--- a/numba/pythonapi.py
+++ b/numba/pythonapi.py
@@ -260,6 +260,15 @@ def err_set_string(self, exctype, msg):
             msg = self.context.insert_const_string(self.module, msg)
         return self.builder.call(fn, (exctype, msg))
 
+    def err_format(self, exctype, msg, *format_args):
+        fnty = Type.function(Type.void(), [self.pyobj, self.cstring], var_arg=True)
+        fn = self._get_function(fnty, name="PyErr_Format")
+        if isinstance(exctype, str):
+            exctype = self.get_c_object(exctype)
+        if isinstance(msg, str):
+            msg = self.context.insert_const_string(self.module, msg)
+        return self.builder.call(fn, (exctype, msg) + tuple(format_args))
+
     def raise_object(self, exc=None):
         """
         Raise an arbitrary exception (type or value or (type, args)
diff --git a/numba/targets/base.py b/numba/targets/base.py
--- a/numba/targets/base.py
+++ b/numba/targets/base.py
@@ -809,7 +809,6 @@ def make_tuple(self, builder, typ, values):
         """
         Create a tuple of the given *typ* containing the *values*.
         """
-        # XXX Use cgutils.pack_array / cgutils.make_anonymous_struct ?
         tup = self.get_constant_undef(typ)
         for i, val in enumerate(values):
             tup = builder.insert_value(tup, val, i)
diff --git a/numba/targets/boxing.py b/numba/targets/boxing.py
--- a/numba/targets/boxing.py
+++ b/numba/targets/boxing.py
@@ -396,29 +396,46 @@ def unbox_tuple(typ, obj, c):
     n = len(typ)
     values = []
     cleanups = []
-    is_error = cgutils.false_bit
+    lty = c.context.get_value_type(typ)
+
+    is_error_ptr = cgutils.alloca_once_value(c.builder, cgutils.false_bit)
+    value_ptr = cgutils.alloca_once(c.builder, lty)
+
+    # Issue #1638: need to check the tuple size
+    actual_size = c.pyapi.tuple_size(obj)
+    size_matches = c.builder.icmp_unsigned('==', actual_size,
+                                            ir.Constant(actual_size.type, n))
+    with c.builder.if_then(c.builder.not_(size_matches), likely=False):
+        c.pyapi.err_format(
+            "PyExc_ValueError",
+            "size mismatch for tuple, expected %d element(s) but got %%zd" % (n,),
+            actual_size)
+        c.builder.store(cgutils.true_bit, is_error_ptr)
+
+    # We unbox the items even if not `size_matches`, to avoid issues with
+    # the generated IR (instruction doesn't dominate all uses)
     for i, eltype in enumerate(typ):
         elem = c.pyapi.tuple_getitem(obj, i)
         native = c.unbox(eltype, elem)
         values.append(native.value)
-        is_error = c.builder.or_(is_error, native.is_error)
+        with c.builder.if_then(native.is_error, likely=False):
+            c.builder.store(cgutils.true_bit, is_error_ptr)
         if native.cleanup is not None:
             cleanups.append(native.cleanup)
 
+    value = c.context.make_tuple(c.builder, typ, values)
+    c.builder.store(value, value_ptr)
+
     if cleanups:
-        def cleanup():
-            for func in reversed(cleanups):
-                func()
+        with c.builder.if_then(size_matches, likely=True):
+            def cleanup():
+                for func in reversed(cleanups):
+                    func()
     else:
         cleanup = None
 
-    # XXX should we use context.make_tuple()
-    if isinstance(typ, types.UniTuple):
-        value = cgutils.pack_array(c.builder, values,
-                                   c.context.get_value_type(typ.dtype))
-    else:
-        value = cgutils.make_anonymous_struct(c.builder, values)
-    return NativeValue(value, is_error=is_error, cleanup=cleanup)
+    return NativeValue(c.builder.load(value_ptr), cleanup=cleanup,
+                       is_error=c.builder.load(is_error_ptr))
 
 
 @box(types.List)


Test patch:
diff --git a/numba/tests/test_lists.py b/numba/tests/test_lists.py
--- a/numba/tests/test_lists.py
+++ b/numba/tests/test_lists.py
@@ -776,6 +776,13 @@ def test_errors(self):
             cfunc((1, [1, 2j]))
         with self.assert_type_error(msg):
             cfunc(Point(1, [1, 2j]))
+        # Issue #1638: tuples of different size.
+        # Note the check is really on the tuple side.
+        lst = [(1,), (2, 3)]
+        with self.assertRaises(ValueError) as raises:
+            cfunc(lst)
+        self.assertEqual(str(raises.exception),
+                         "size mismatch for tuple, expected 1 element(s) but got 2")
 
 
 class TestListReflection(MemoryLeakMixin, TestCase):
diff --git a/numba/tests/test_tuples.py b/numba/tests/test_tuples.py
--- a/numba/tests/test_tuples.py
+++ b/numba/tests/test_tuples.py
@@ -135,6 +135,15 @@ def test_hetero_tuple(self):
         self.assertPreciseEqual(cr_first.entry_point((2**61, 1.5)), 2**61)
         self.assertPreciseEqual(cr_second.entry_point((2**61, 1.5)), 1.5)
 
+    def test_size_mismatch(self):
+        # Issue #1638: tuple size should be checked when unboxing
+        tuple_type = types.UniTuple(types.int32, 2)
+        cr = compile_isolated(tuple_first, (tuple_type,))
+        with self.assertRaises(ValueError) as raises:
+            cr.entry_point((4, 5, 6))
+        self.assertEqual(str(raises.exception),
+                         "size mismatch for tuple, expected 2 element(s) but got 3")
+
 
 class TestOperations(TestCase):