Repository: numba/numba

Issue:
Python crashes when running a script if a particular function is `jit`-ted
Here is the script for reproducing the issue: https://gist.github.com/bmer/09c7a129e3fe80a97158

It is specific to causing the issue, and I have added some simple commenting.

It should run as is, printing out the correct results.

If line 103 (a `jit` type specification) is uncommented (it is currently commented, and has no effect), then the script will crash (on Windows: "python.exe has stopped working"). 

I do not know enough about `numba` to say more, but I would be happy to provide additional information.



Discussion:
Crash confirmed on OSX with python2.7 numpy1.9


Resolving patch:
diff --git a/numba/lowering.py b/numba/lowering.py
--- a/numba/lowering.py
+++ b/numba/lowering.py
@@ -272,6 +272,7 @@ def lower_inst(self, inst):
                 pass
             else:
                 self.decref(self.typeof(inst.value), val)
+                self._delete_variable(inst.value)
 
         elif isinstance(inst, ir.SetAttr):
             target = self.loadvar(inst.target.name)
@@ -725,3 +726,10 @@ def decref(self, typ, val):
             return
 
         self.context.nrt_decref(self.builder, typ, val)
+
+    def _delete_variable(self, varname):
+        """
+        Zero-fill variable to avoid crashing due to extra ir.Del
+        """
+        storage = self.getvar(varname)
+        self.builder.store(Constant.null(storage.type.pointee), storage)
diff --git a/numba/npyufunc/array_exprs.py b/numba/npyufunc/array_exprs.py
--- a/numba/npyufunc/array_exprs.py
+++ b/numba/npyufunc/array_exprs.py
@@ -1,5 +1,6 @@
 from __future__ import print_function, division, absolute_import
 import ast
+from collections import defaultdict
 
 from numpy import ufunc
 
@@ -125,7 +126,7 @@ def _handle_matches(self):
         '''
         replace_map = {}
         dead_vars = set()
-        used_vars = set()
+        used_vars = defaultdict(int)
         for match in self.matches:
             instr = self.array_assigns[match]
             expr = instr.value
@@ -144,8 +145,8 @@ def _handle_matches(self):
                     child_assign = self.array_assigns[operand_name]
                     child_expr = child_assign.value
                     child_operands = child_expr.list_vars()
-                    used_vars.update(operand.name
-                                     for operand in child_operands)
+                    for operand in child_operands:
+                        used_vars[operand.name] += 1
                     arr_inps.append(self._translate_expr(child_expr))
                     if child_assign.target.is_temp:
                         dead_vars.add(child_assign.target.name)
@@ -153,7 +154,7 @@ def _handle_matches(self):
                 elif operand_name in self.const_assigns:
                     arr_inps.append(self.const_assigns[operand_name])
                 else:
-                    used_vars.add(operand.name)
+                    used_vars[operand.name] += 1
                     arr_inps.append(operand)
         return replace_map, dead_vars, used_vars
 
@@ -180,7 +181,6 @@ def apply(self):
         delete_map = {}
         for instr in self.crnt_block.body:
             if isinstance(instr, ir.Assign):
-                target_name = instr.target.name
                 if instr in replace_map:
                     replacement = self._get_final_replacement(
                         replace_map, instr)
@@ -190,14 +190,15 @@ def apply(self):
                             var_name = var.name
                             if var_name in delete_map:
                                 result.append(delete_map.pop(var_name))
-                            if var_name in used_vars:
-                                used_vars.remove(var_name)
+                            if used_vars[var_name] > 0:
+                                used_vars[var_name] -= 1
+
                 else:
                     result.append(instr)
             elif isinstance(instr, ir.Del):
                 instr_value = instr.value
-                if instr_value in used_vars:
-                    used_vars.remove(instr_value)
+                if used_vars[instr_value] > 0:
+                    used_vars[instr_value] -= 1
                     delete_map[instr_value] = instr
                 elif instr_value not in dead_vars:
                     result.append(instr)


Test patch:
diff --git a/numba/tests/test_func_lifetime.py b/numba/tests/test_func_lifetime.py
--- a/numba/tests/test_func_lifetime.py
+++ b/numba/tests/test_func_lifetime.py
@@ -130,5 +130,37 @@ def test_inner_function_lifetime_npm(self):
         self.check_inner_function_lifetime(nopython=True)
 
 
+class TestLifeTimeIssue(TestCase):
+    def test_double_free(self):
+        from numba import njit
+        import numpy as np
+
+        # This is the function that causes the crash
+
+        @njit
+        def is_point_in_polygons(point, polygons):
+            num_polygons = polygons.shape[0]
+            if num_polygons != 0:
+                # An extra decref is inserted in this block
+                intentionally_unused_variable = polygons[0]
+            return 0
+
+        # This function creates some NRT objects for the previous function
+        # to corrupt.
+
+        @njit
+        def dummy():
+            return np.empty(10, dtype=np.int64)
+
+        polygons = np.array([[[0, 1]]])
+        points = np.array([[-1.5, 0.5]])
+        a = dummy()
+        is_point_in_polygons(points[0], polygons)
+        b = dummy()
+        # Crash happens at second call
+        is_point_in_polygons(points[0], polygons)
+        c = dummy()
+
+
 if __name__ == '__main__':
     unittest.main()