Repository: numba/numba

Issue:
Multiple assignment gives wrong result.
While there is a old (and solved) issue for multiple assignment, i still have this serve bug (it gives silently a wrong result.). Win 64, Python 2.7, numba 0.13.2 and llvmpy 0.12.6 from c.gohlke.

``` python
@jit
def draw_line(x0, y0, x1, y1):
    print(x0, y0, x1, y1)
    x0, y0 = y0, x0
    x1, y1 = y1, x1
    print(x0, y0, x1, y1)  #should be (10.0, 10.0, 50.0, 30.0), with jit: (10.0, 10.0, 50.0, 50.0)

draw_line(10.,10., 30., 50.)
```



Discussion:

Resolving patch:
diff --git a/numba/compiler.py b/numba/compiler.py
--- a/numba/compiler.py
+++ b/numba/compiler.py
@@ -312,9 +312,6 @@ def native_lowering_stage(targetctx, interp, typemap, restype, calltypes,
 
 
 def py_lowering_stage(targetctx, interp, nocompile):
-    # Optimize for python code
-    ir_optimize_for_py_stage(interp)
-
     fndesc = lowering.PythonFunctionDescriptor.from_object_mode_function(interp)
     lower = lowering.PyLower(targetctx, fndesc)
     lower.lower()
diff --git a/numba/dataflow.py b/numba/dataflow.py
--- a/numba/dataflow.py
+++ b/numba/dataflow.py
@@ -190,7 +190,9 @@ def op_STORE_MAP(self, info, inst):
 
     def op_LOAD_FAST(self, info, inst):
         name = self.bytecode.co_varnames[inst.arg]
-        info.push(name)
+        res = info.make_temp(name)
+        info.append(inst, res=res)
+        info.push(res)
 
     def op_LOAD_CONST(self, info, inst):
         res = info.make_temp('const')
diff --git a/numba/interpreter.py b/numba/interpreter.py
--- a/numba/interpreter.py
+++ b/numba/interpreter.py
@@ -4,11 +4,62 @@
     import __builtin__ as builtins
 except ImportError:
     import builtins
-import sys
+import collections
 import dis
+import sys
+
 from numba import ir, controlflow, dataflow, utils
 
 
+class Assigner(object):
+    """
+    This object keeps track of potential assignment simplifications
+    inside a code block.
+    For example `$O.1 = x` followed by `y = $0.1` can be simplified
+    into `y = x`, but it's not possible anymore if we have `x = z`
+    in-between those two instructions.
+
+    NOTE: this is not only an optimization, but is actually necessary
+    due to certain limitations of Numba - such as only accepting the
+    returning of an array passed as function argument.
+    """
+
+    def __init__(self):
+        # { destination variable name -> source Var object }
+        self.dest_to_src = {}
+        self.src_invalidate = collections.defaultdict(list)
+        self.unused_dests = set()
+
+    def assign(self, srcvar, destvar):
+        """
+        Assign *srcvar* to *destvar*. Return either *srcvar* or a possible
+        simplified assignment source (earlier assigned to *srcvar*).
+        """
+        srcname = srcvar.name
+        destname = destvar.name
+        if srcname in self.src_invalidate:
+            # Invalidate all previously known simplifications
+            for d in self.src_invalidate.pop(srcname):
+                self.dest_to_src.pop(d)
+        if srcname in self.dest_to_src:
+            srcvar = self.dest_to_src[srcname]
+        if destvar.is_temp:
+            self.dest_to_src[destname] = srcvar
+            self.src_invalidate[srcname].append(destname)
+            self.unused_dests.add(destname)
+        return srcvar
+
+    def get_assignment_source(self, destname):
+        """
+        Get a possible assignment source (a ir.Var instance) to replace
+        *destname*, otherwise None.
+        """
+        if destname in self.dest_to_src:
+            return self.dest_to_src[destname]
+        self.unused_dests.discard(destname)
+        return None
+
+
 class Interpreter(object):
     """A bytecode interpreter that builds up the IR.
     """
@@ -101,13 +152,29 @@ def _start_new_block(self, inst):
             oldblock.append(jmp)
             # Get DFA block info
         self.dfainfo = self.dfa.infos[self.current_block_offset]
+        self.assigner = Assigner()
         # Notify listeners for the new block
         for fn in utils.dict_itervalues(self._block_actions):
             fn(self.current_block_offset, self.current_block)
 
     def _end_current_block(self):
+        self._remove_unused_temporaries()
         self._insert_outgoing_phis()
 
+    def _remove_unused_temporaries(self):
+        """
+        Remove assignments to unused temporary variables from the
+        current block.
+        """
+        new_body = []
+        for inst in self.current_block.body:
+            if (isinstance(inst, ir.Assign)
+                and inst.target.is_temp
+                and inst.target.name in self.assigner.unused_dests):
+                continue
+            new_body.append(inst)
+        self.current_block.body = new_body
+
     def _insert_outgoing_phis(self):
         """
         Add assignments to forward requested outgoing values
@@ -178,21 +245,26 @@ def dump(self, file=None):
     # --- Scope operations ---
 
     def store(self, value, name, redefine=False):
+        """
+        Store *value* (a Var instance) into the variable named *name*
+        (a str object).
+        """
         if redefine or self.current_block_offset in self.cfa.backbone:
             target = self.current_scope.redefine(name, loc=self.loc)
         else:
             target = self.current_scope.get_or_define(name, loc=self.loc)
+        if isinstance(value, ir.Var):
+            value = self.assigner.assign(value, target)
         stmt = ir.Assign(value=value, target=target, loc=self.loc)
         self.current_block.append(stmt)
 
-    # def store_temp(self, value):
-    #     target = self.current_scope.make_temp(loc=self.loc)
-    #     stmt = ir.Assign(value=value, target=target, loc=self.loc)
-    #     self.current_block.append(stmt)
-    #     return target
-
     def get(self, name):
-        return self.current_scope.get(name)
+        # Try to simplify the variable lookup by returning an earlier
+        # variable assigned to *name*.
+        var = self.assigner.get_assignment_source(name)
+        if var is None:
+            var = self.current_scope.get(name)
+        return var
 
     # --- Block operations ---
 
@@ -396,6 +468,10 @@ def op_STORE_SLICE_3(self, inst, base, start, stop, value, slicevar,
                           loc=self.loc)
         self.current_block.append(stmt)
 
+    def op_LOAD_FAST(self, inst, res):
+        srcname = self.code_locals[inst.arg]
+        self.store(value=self.get(srcname), name=res)
+
     def op_STORE_FAST(self, inst, value):
         dstname = self.code_locals[inst.arg]
         value = self.get(value)


Test patch:
diff --git a/numba/tests/test_dataflow.py b/numba/tests/test_dataflow.py
--- a/numba/tests/test_dataflow.py
+++ b/numba/tests/test_dataflow.py
@@ -65,6 +65,14 @@ def stack_effect_error(x):
     return i + c
 
 
+# Issue #571
+def var_swapping(a, b, c, d, e):
+    a, b = b, a
+    c, d, e = e, c, d
+    a, b, c, d = b, c, d, a
+    return a + b + c + d +e
+
+
 class TestDataFlow(TestCase):
 
     def setUp(self):
@@ -152,6 +160,16 @@ def test_stack_effect_error(self, flags=force_pyobj_flags):
     def test_stack_effect_error_npm(self):
         self.test_stack_effect_error(no_pyobj_flags)
 
+    def test_var_swapping(self, flags=force_pyobj_flags):
+        pyfunc = var_swapping
+        cr = compile_isolated(pyfunc, (types.int32,) * 5, flags=flags)
+        cfunc = cr.entry_point
+        args = tuple(range(0, 10, 2))
+        self.assertPreciseEqual(pyfunc(*args), cfunc(*args))
+
+    def test_var_swapping_npm(self):
+        self.test_var_swapping(no_pyobj_flags)
+
 
 if __name__ == '__main__':
     unittest.main()