Repository: numba/numba

Issue:
bytecode analysis failure with `BUILD_TUPLE_UNPACK`.
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

```python
from numba import njit

@njit
def foo(x):
    return (*x,)

foo((1,2))
```
does this:
```
<path>/numba/core/byteflow.py in _build_tuple_unpack(self, state, inst)
    846         state.append(inst, tuples=tuples, temps=temps)
    847         # The result is in the last temp var
--> 848         state.push(temps[-1])
    849 
    850     def op_BUILD_TUPLE_UNPACK_WITH_CALL(self, state, inst):

IndexError: Failed in nopython mode pipeline (step: analyzing bytecode)
list index out of range
```

Discussion:

Patch:
diff --git a/numba/core/byteflow.py b/numba/core/byteflow.py
index 69abff298..f84ac33b9 100644
--- a/numba/core/byteflow.py
+++ b/numba/core/byteflow.py
@@ -846,7 +846,14 @@ class TraceRunner(object):
         # Builds tuple from other tuples on the stack
         tuples = list(reversed([state.pop() for _ in range(inst.arg)]))
         temps = [state.make_temp() for _ in range(len(tuples) - 1)]
-        state.append(inst, tuples=tuples, temps=temps)
+
+        # if the unpack is assign-like, e.g. x = (*y,), it needs handling
+        # differently.
+        is_assign = len(tuples) == 1
+        if is_assign:
+            temps = [state.make_temp(),]
+
+        state.append(inst, tuples=tuples, temps=temps, is_assign=is_assign)
         # The result is in the last temp var
         state.push(temps[-1])
 
diff --git a/numba/core/dataflow.py b/numba/core/dataflow.py
index 1665d8edc..4b31f9172 100644
--- a/numba/core/dataflow.py
+++ b/numba/core/dataflow.py
@@ -333,7 +333,13 @@ class DataFlowAnalysis(object):
         # Builds tuple from other tuples on the stack
         tuples = list(reversed([info.pop() for _ in range(inst.arg)]))
         temps = [info.make_temp() for _ in range(len(tuples) - 1)]
-        info.append(inst, tuples=tuples, temps=temps)
+        # if the unpack is assign-like, e.g. x = (*y,), it needs handling
+        # differently.
+        is_assign = len(tuples) == 1
+        if is_assign:
+            temps = [info.make_temp(),]
+
+        info.append(inst, tuples=tuples, temps=temps, is_assign=is_assign)
         # The result is in the last temp var
         info.push(temps[-1])
 
diff --git a/numba/core/interpreter.py b/numba/core/interpreter.py
index 92ed3481c..2a0d2066c 100644
--- a/numba/core/interpreter.py
+++ b/numba/core/interpreter.py
@@ -10,6 +10,7 @@ from numba.core.utils import (PYVERSION, BINOPS_TO_OPERATORS,
                               INPLACE_BINOPS_TO_OPERATORS,)
 from numba.core.byteflow import Flow, AdaptDFA, AdaptCFA
 from numba.core.unsafe import eh
+from numba.cpython.unsafe.tuple import unpack_single_tuple
 
 
 class _UNKNOWN_VALUE(object):
@@ -1134,20 +1135,31 @@ class Interpreter(object):
             expr = ir.Expr.call(func, [], [], loc=self.loc, vararg=vararg)
             self.store(expr, res)
 
-    def _build_tuple_unpack(self, inst, tuples, temps):
+    def _build_tuple_unpack(self, inst, tuples, temps, is_assign):
         first = self.get(tuples[0])
-        for other, tmp in zip(map(self.get, tuples[1:]), temps):
-            out = ir.Expr.binop(fn=operator.add, lhs=first, rhs=other,
-                                loc=self.loc)
-            self.store(out, tmp)
-            first = self.get(tmp)
+        if is_assign:
+            # it's assign-like, defer handling to an intrinsic that will have
+            # type information.
+            # Can deal with tuples only, i.e. y = (*x,). where x = <tuple>
+            gv_name = "unpack_single_tuple"
+            gv_fn = ir.Global(gv_name, unpack_single_tuple, loc=self.loc,)
+            self.store(value=gv_fn, name=gv_name, redefine=True)
+            exc = ir.Expr.call(self.get(gv_name), args=(first,), kws=(),
+                               loc=self.loc,)
+            self.store(exc, temps[0])
+        else:
+            for other, tmp in zip(map(self.get, tuples[1:]), temps):
+                out = ir.Expr.binop(fn=operator.add, lhs=first, rhs=other,
+                                    loc=self.loc)
+                self.store(out, tmp)
+                first = self.get(tmp)
 
-    def op_BUILD_TUPLE_UNPACK_WITH_CALL(self, inst, tuples, temps):
+    def op_BUILD_TUPLE_UNPACK_WITH_CALL(self, inst, tuples, temps, is_assign):
         # just unpack the input tuple, call inst will be handled afterwards
-        self._build_tuple_unpack(inst, tuples, temps)
+        self._build_tuple_unpack(inst, tuples, temps, is_assign)
 
-    def op_BUILD_TUPLE_UNPACK(self, inst, tuples, temps):
-        self._build_tuple_unpack(inst, tuples, temps)
+    def op_BUILD_TUPLE_UNPACK(self, inst, tuples, temps, is_assign):
+        self._build_tuple_unpack(inst, tuples, temps, is_assign)
 
     def op_LIST_TO_TUPLE(self, inst, const_list, res):
         expr = ir.Expr.dummy('list_to_tuple', (const_list,), loc=self.loc)
diff --git a/numba/cpython/unsafe/tuple.py b/numba/cpython/unsafe/tuple.py
index bf2393b45..7e30b07c1 100644
--- a/numba/cpython/unsafe/tuple.py
+++ b/numba/cpython/unsafe/tuple.py
@@ -3,7 +3,7 @@ This file provides internal compiler utilities that support certain special
 operations with tuple and workarounds for limitations enforced in userland.
 """
 
-from numba.core import types, typing
+from numba.core import types, typing, errors
 from numba.core.cgutils import alloca_once
 from numba.core.extending import intrinsic
 
@@ -58,3 +58,24 @@ def build_full_slice_tuple(tyctx, sz):
         return res
 
     return sig, codegen
+
+
+@intrinsic
+def unpack_single_tuple(tyctx, tup):
+    """This exists to handle the situation y = (*x,), the interpreter injects a
+    call to it in the case of a single value unpack. It's not possible at
+    interpreting time to differentiate between an unpack on a variable sized
+    container e.g. list and a fixed one, e.g. tuple. This function handles the
+    situation should it arise.
+    """
+    # See issue #6534
+    if not isinstance(tup, types.BaseTuple):
+        msg = (f"Only tuples are supported when unpacking a single item, "
+               f"got type: {tup}")
+        raise errors.UnsupportedError(msg)
+
+    sig = tup(tup)
+
+    def codegen(context, builder, signature, args):
+        return args[0] # there's only one tuple and it's a simple pass through
+    return sig, codegen


Test patch:
diff --git a/numba/tests/test_tuples.py b/numba/tests/test_tuples.py
index d01f28b1b..c671e71cf 100644
--- a/numba/tests/test_tuples.py
+++ b/numba/tests/test_tuples.py
@@ -633,8 +633,7 @@ class TestTupleBuild(TestCase):
 
     def test_build_unpack(self):
         def check(p):
-            # using eval here since Python 2 doesn't even support the syntax
-            pyfunc = eval("lambda a: (1, *a)")
+            pyfunc = lambda a: (1, *a)
             cfunc = jit(nopython=True)(pyfunc)
             self.assertPreciseEqual(cfunc(p), pyfunc(p))
 
@@ -643,10 +642,46 @@ class TestTupleBuild(TestCase):
         # Heterogeneous
         check((4, 5.5))
 
+    def test_build_unpack_assign_like(self):
+        # see #6534
+        def check(p):
+            pyfunc = lambda a: (*a,)
+            cfunc = jit(nopython=True)(pyfunc)
+            self.assertPreciseEqual(cfunc(p), pyfunc(p))
+
+        # Homogeneous
+        check((4, 5))
+        # Heterogeneous
+        check((4, 5.5))
+
+    def test_build_unpack_fail_on_list_assign_like(self):
+        # see #6534
+        def check(p):
+            pyfunc = lambda a: (*a,)
+            cfunc = jit(nopython=True)(pyfunc)
+            self.assertPreciseEqual(cfunc(p), pyfunc(p))
+
+        with self.assertRaises(errors.TypingError) as raises:
+            check([4, 5])
+
+        if utils.PYVERSION > (3, 8):
+            # Python 3.9 has a peephole rewrite due to large changes in tuple
+            # unpacking. It results in a tuple + list situation from the above
+            # so the error message reflects that. Catching this specific and
+            # seemingly rare sequence in the peephole rewrite is prohibitively
+            # hard. Should it be reported numerous times, revisit then.
+            msg1 = "No implementation of function"
+            self.assertIn(msg1, str(raises.exception))
+            msg2 = "add(Tuple()," # ignore the reflected list part, it's repr
+                                  # is quite volatile.
+            self.assertIn(msg2, str(raises.exception))
+        else:
+            msg = "Only tuples are supported when unpacking a single item"
+            self.assertIn(msg, str(raises.exception))
+
     def test_build_unpack_more(self):
         def check(p):
-            # using eval here since Python 2 doesn't even support the syntax
-            pyfunc = eval("lambda a: (1, *a, (1, 2), *a)")
+            pyfunc = lambda a: (1, *a, (1, 2), *a)
             cfunc = jit(nopython=True)(pyfunc)
             self.assertPreciseEqual(cfunc(p), pyfunc(p))
 
@@ -657,11 +692,10 @@ class TestTupleBuild(TestCase):
 
     def test_build_unpack_call(self):
         def check(p):
-            # using eval here since Python 2 doesn't even support the syntax
             @jit
             def inner(*args):
                 return args
-            pyfunc = eval("lambda a: inner(1, *a)", locals())
+            pyfunc = lambda a: inner(1, *a)
             cfunc = jit(nopython=True)(pyfunc)
             self.assertPreciseEqual(cfunc(p), pyfunc(p))
 
@@ -670,14 +704,12 @@ class TestTupleBuild(TestCase):
         # Heterogeneous
         check((4, 5.5))
 
-    @unittest.skipIf(utils.PYVERSION < (3, 6), "needs Python 3.6+")
     def test_build_unpack_call_more(self):
         def check(p):
-            # using eval here since Python 2 doesn't even support the syntax
             @jit
             def inner(*args):
                 return args
-            pyfunc = eval("lambda a: inner(1, *a, *(1, 2), *a)", locals())
+            pyfunc = lambda a: inner(1, *a, *(1, 2), *a)
             cfunc = jit(nopython=True)(pyfunc)
             self.assertPreciseEqual(cfunc(p), pyfunc(p))