Repository: numba/numba

Issue:
Inconsistent stack effect 
Triggered by the following code:

``` python
def stack_effect_error():
    i = 2
    c = 1
    if i == 0:
        for i in range(2):
            c = i

    return i + c
```

And by https://github.com/numba/numba/blob/master/numba/cuda/tests/cudapy/test_globals.py



Discussion:

Resolving patch:
diff --git a/numba/dataflow.py b/numba/dataflow.py
--- a/numba/dataflow.py
+++ b/numba/dataflow.py
@@ -83,6 +83,23 @@ def dup_topx(self, info, inst, count):
         for val in duped:
             info.push(val)
 
+    def add_syntax_block(self, info, block):
+        """
+        Add an inner syntax block.
+        """
+        block.stack_offset = info.stack_offset
+        self.syntax_blocks.append(block)
+
+    def pop_syntax_block(self, info):
+        """
+        Pop the innermost syntax block and revert its stack effect.
+        """
+        block = self.syntax_blocks.pop()
+        assert info.stack_offset >= block.stack_offset
+        while info.stack_offset + info.stack_effect > block.stack_offset:
+            info.pop(discard=True)
+        return block
+
     def op_DUP_TOPX(self, info, inst):
         count = inst.arg
         assert 1 <= count <= 5, "Invalid DUP_TOPX count"
@@ -480,13 +497,13 @@ def op_RETURN_VALUE(self, info, inst):
         info.terminator = inst
 
     def op_SETUP_LOOP(self, info, inst):
-        self.syntax_blocks.append(LoopBlock())
+        self.add_syntax_block(info, LoopBlock())
         info.append(inst)
 
     def op_POP_BLOCK(self, info, inst):
-        block = self.syntax_blocks.pop()
+        block = self.pop_syntax_block(info)
         if isinstance(block, LoopBlock):
-            info.append(inst, delitem=block.iterator)
+            info.append(inst, delitems=[block.iterator])
         else:
             info.append(inst)
 
@@ -501,10 +518,11 @@ def _ignored(self, info, inst):
 
 
 class LoopBlock(object):
-    __slots__ = 'iterator'
+    __slots__ = ('iterator', 'stack_offset')
 
     def __init__(self):
         self.iterator = None
+        self.stack_offset = None
 
 
 class BlockInfo(object):
diff --git a/numba/interpreter.py b/numba/interpreter.py
--- a/numba/interpreter.py
+++ b/numba/interpreter.py
@@ -655,10 +655,10 @@ def op_JUMP_FORWARD(self, inst):
         jmp = ir.Jump(inst.get_jump_target(), loc=self.loc)
         self.current_block.append(jmp)
 
-    def op_POP_BLOCK(self, inst, delitem=None):
+    def op_POP_BLOCK(self, inst, delitems=()):
         blk = self.syntax_blocks.pop()
-        if delitem is not None:
-            delete = ir.Del(delitem, loc=self.loc)
+        for item in delitems:
+            delete = ir.Del(item, loc=self.loc)
             self.current_block.append(delete)
         if blk in self._block_actions:
             del self._block_actions[blk]


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
@@ -1,5 +1,7 @@
 from __future__ import print_function
+
 import warnings
+
 import numba.unittest_support as unittest
 from numba.compiler import compile_isolated, Flags
 from numba.utils import PYVERSION
@@ -53,13 +55,13 @@ def chained_compare(a):
     return 1 < a < 3
 
 
-def stack_effect_error():
+# Issue #591
+def stack_effect_error(x):
     i = 2
     c = 1
-    if i == 0:
-        for i in range(2):
+    if i == x:
+        for i in range(3):
             c = i
-
     return i + c
 
 
@@ -67,6 +69,13 @@ class TestDataFlow(TestCase):
 
     def setUp(self):
         self.cache = CompilationCache()
+        # All tests here should run without warnings
+        self.w_cm = warnings.catch_warnings()
+        self.w_cm.__enter__()
+        warnings.simplefilter("error")
+
+    def tearDown(self):
+        self.w_cm.__exit__(None, None, None)
 
     def test_assignments(self, flags=force_pyobj_flags):
         pyfunc = assignments
@@ -131,14 +140,17 @@ def test_chained_compare(self, flags=force_pyobj_flags):
     def test_chained_compare_npm(self):
         self.test_chained_compare(no_pyobj_flags)
 
-    @unittest.expectedFailure
     def test_stack_effect_error(self, flags=force_pyobj_flags):
+        # Issue #591: POP_BLOCK must undo all stack pushes done inside
+        # the block.
         pyfunc = stack_effect_error
+        cr = compile_isolated(pyfunc, (types.int32,), flags=flags)
+        cfunc = cr.entry_point
+        for x in (0, 1, 2, 3):
+            self.assertPreciseEqual(pyfunc(x), cfunc(x))
 
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter("error")
-            cr = compile_isolated(pyfunc, (), flags=flags)
-
+    def test_stack_effect_error_npm(self):
+        self.test_stack_effect_error(no_pyobj_flags)
 
 
 if __name__ == '__main__':