Repository: numba/numba

Issue:
Regression 0.49RC1 vs 0.48 (possibly SSA related)
Hi, the following works in pure python and in Numba v0.48, but it's not working in 0.49rc.

Sorry that the code is a bit convoluted, couldn't reduce it more than this.

cc: @stuartarchibald 

```python
from numba import njit
import numpy as np

pol_data = np.array([(1)], dtype=[('max_t', 'i8'), ])[0]

ent_vec = ((np.ones(100), np.ones(100)),(np.ones(100), np.ones(100)))
decr_vecs = (ent_vec,)

@njit
def foo(pol_data, decr_vecs):
    nr_ind_states = (2, )
    nr_jnt_states = np.prod(np.array(nr_ind_states))
    nr_entities = len(nr_ind_states)
    nr_jnt_trans = nr_jnt_states ** 2
    nr_jnt_probs = nr_jnt_states + nr_jnt_trans
    probs = np.zeros((pol_data['max_t'] + 1, nr_jnt_probs), np.float64)
    t = 1
    for ent_idx in range(1):
        v = np.array([1,0])
        ent_uncond_probs = np.ones((2,2))
        if ent_idx == 0:
            if ent_idx == nr_entities - 1:
                pass
            else:
                cum_v = v
        else:
            if ent_idx == nr_entities - 1:
                pass
            else:
                cum_v = np.kron(cum_v, v)
    return probs

foo(pol_data, decr_vecs)
```

<details><summary>Long error message here </summary>
<p>

```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/errors.py in new_error_context(fmt_, *args, **kwargs)
    768     try:
--> 769         yield
    770     except NumbaError as e:

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in lower_block(self, block)
    266                                    loc=self.loc, errcls_=defaulterrcls):
--> 267                 self.lower_inst(inst)
    268         self.post_block(block)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in lower_inst(self, inst)
    363             ty = self.typeof(inst.target.name)
--> 364             val = self.lower_assign(ty, inst)
    365             self.storevar(val, inst.target.name)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in lower_assign(self, ty, inst)
    540         elif isinstance(value, ir.Var):
--> 541             val = self.loadvar(value.name)
    542             oty = self.typeof(value.name)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in loadvar(self, name)
   1280         """
-> 1281         ptr = self.getvar(name)
   1282         return self.builder.load(ptr)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in getvar(self, name)
   1274         """
-> 1275         return self.varmap[name]
   1276 

KeyError: 'cum_v.2'

During handling of the above exception, another exception occurred:

LoweringError                             Traceback (most recent call last)
<ipython-input-19-fa852184280b> in <module>
     31     return probs
     32 
---> 33 foo(pol_data, decr_vecs)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/dispatcher.py in _compile_for_args(self, *args, **kws)
    418                     e.patch_message('\n'.join((str(e).rstrip(), help_msg)))
    419             # ignore the FULL_TRACEBACKS config, this needs reporting!
--> 420             raise e
    421 
    422     def inspect_llvm(self, signature=None):

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/dispatcher.py in _compile_for_args(self, *args, **kws)
    351                 argtypes.append(self.typeof_pyval(a))
    352         try:
--> 353             return self.compile(tuple(argtypes))
    354         except errors.ForceLiteralArg as e:
    355             # Received request for compiler re-entry with the list of arguments

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler_lock.py in _acquire_compile_lock(*args, **kwargs)
     30         def _acquire_compile_lock(*args, **kwargs):
     31             with self:
---> 32                 return func(*args, **kwargs)
     33         return _acquire_compile_lock
     34 

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/dispatcher.py in compile(self, sig)
    792             self._cache_misses[sig] += 1
    793             try:
--> 794                 cres = self._compiler.compile(args, return_type)
    795             except errors.ForceLiteralArg as e:
    796                 def folded(args, kws):

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/dispatcher.py in compile(self, args, return_type)
     75 
     76     def compile(self, args, return_type):
---> 77         status, retval = self._compile_cached(args, return_type)
     78         if status:
     79             return retval

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/dispatcher.py in _compile_cached(self, args, return_type)
     89 
     90         try:
---> 91             retval = self._compile_core(args, return_type)
     92         except errors.TypingError as e:
     93             self._failed_cache[key] = e

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/dispatcher.py in _compile_core(self, args, return_type)
    102 
    103         impl = self._get_implementation(args, {})
--> 104         cres = compiler.compile_extra(self.targetdescr.typing_context,
    105                                       self.targetdescr.target_context,
    106                                       impl,

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler.py in compile_extra(typingctx, targetctx, func, args, return_type, flags, locals, library, pipeline_class)
    566     pipeline = pipeline_class(typingctx, targetctx, library,
    567                               args, return_type, flags, locals)
--> 568     return pipeline.compile_extra(func)
    569 
    570 

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler.py in compile_extra(self, func)
    337         self.state.lifted = ()
    338         self.state.lifted_from = None
--> 339         return self._compile_bytecode()
    340 
    341     def compile_ir(self, func_ir, lifted=(), lifted_from=None):

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler.py in _compile_bytecode(self)
    399         """
    400         assert self.state.func_ir is None
--> 401         return self._compile_core()
    402 
    403     def _compile_ir(self):

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler.py in _compile_core(self)
    379                 self.state.status.fail_reason = e
    380                 if is_final_pipeline:
--> 381                     raise e
    382         else:
    383             raise CompilerError("All available pipelines exhausted")

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler.py in _compile_core(self)
    370             res = None
    371             try:
--> 372                 pm.run(self.state)
    373                 if self.state.cr is not None:
    374                     break

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler_machinery.py in run(self, state)
    339                     (self.pipeline_name, pass_desc)
    340                 patched_exception = self._patch_error(msg, e)
--> 341                 raise patched_exception
    342 
    343     def dependency_analysis(self):

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler_machinery.py in run(self, state)
    330                 pass_inst = _pass_registry.get(pss).pass_inst
    331                 if isinstance(pass_inst, CompilerPass):
--> 332                     self._runPass(idx, pass_inst, state)
    333                 else:
    334                     raise BaseException("Legacy pass in use")

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler_lock.py in _acquire_compile_lock(*args, **kwargs)
     30         def _acquire_compile_lock(*args, **kwargs):
     31             with self:
---> 32                 return func(*args, **kwargs)
     33         return _acquire_compile_lock
     34 

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler_machinery.py in _runPass(self, index, pss, internal_state)
    289             mutated |= check(pss.run_initialization, internal_state)
    290         with SimpleTimer() as pass_time:
--> 291             mutated |= check(pss.run_pass, internal_state)
    292         with SimpleTimer() as finalize_time:
    293             mutated |= check(pss.run_finalizer, internal_state)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/compiler_machinery.py in check(func, compiler_state)
    262 
    263         def check(func, compiler_state):
--> 264             mangled = func(compiler_state)
    265             if mangled not in (True, False):
    266                 msg = ("CompilerPass implementations should return True/False. "

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/typed_passes.py in run_pass(self, state)
    440 
    441         # TODO: Pull this out into the pipeline
--> 442         NativeLowering().run_pass(state)
    443         lowered = state['cr']
    444         signature = typing.signature(state.return_type, *state.args)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/typed_passes.py in run_pass(self, state)
    368                 lower = lowering.Lower(targetctx, library, fndesc, interp,
    369                                        metadata=metadata)
--> 370                 lower.lower()
    371                 if not flags.no_cpython_wrapper:
    372                     lower.create_cpython_wrapper(flags.release_gil)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in lower(self)
    177         if self.generator_info is None:
    178             self.genlower = None
--> 179             self.lower_normal_function(self.fndesc)
    180         else:
    181             self.genlower = self.GeneratorLower(self)

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in lower_normal_function(self, fndesc)
    225         # Init argument values
    226         self.extract_function_arguments()
--> 227         entry_block_tail = self.lower_function_body()
    228 
    229         # Close tail of entry block

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in lower_function_body(self)
    251             bb = self.blkmap[offset]
    252             self.builder.position_at_end(bb)
--> 253             self.lower_block(block)
    254         self.post_lower()
    255         return entry_block_tail

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/lowering.py in lower_block(self, block)
    265             with new_error_context('lowering "{inst}" at {loc}', inst=inst,
    266                                    loc=self.loc, errcls_=defaulterrcls):
--> 267                 self.lower_inst(inst)
    268         self.post_block(block)
    269 

~/anaconda3/envs/latest_versions38/lib/python3.8/contextlib.py in __exit__(self, type, value, traceback)
    129                 value = type()
    130             try:
--> 131                 self.gen.throw(type, value, traceback)
    132             except StopIteration as exc:
    133                 # Suppress StopIteration *unless* it's the same exception that

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/errors.py in new_error_context(fmt_, *args, **kwargs)
    774         newerr = errcls(e).add_context(_format_msg(fmt_, args, kwargs))
    775         tb = sys.exc_info()[2] if numba.core.config.FULL_TRACEBACKS else None
--> 776         reraise(type(newerr), newerr, tb)
    777 
    778 

~/anaconda3/envs/latest_versions38/lib/python3.8/site-packages/numba/core/utils.py in reraise(tp, value, tb)
     78     if value.__traceback__ is not tb:
     79         raise value.with_traceback(tb)
---> 80     raise value
     81 
     82 

LoweringError: Failed in nopython mode pipeline (step: nopython mode backend)
'cum_v.2'

File "<ipython-input-19-fa852184280b>", line 30:
def foo(pol_data, decr_vecs):
    <source elided>
            else:
                cum_v = np.kron(cum_v, v)
                ^

[1] During: lowering "cum_v.4 = cum_v.2" at <ipython-input-19-fa852184280b> (30)
```
</p>
</details>

Discussion:

Patch:
diff --git a/numba/core/config.py b/numba/core/config.py
index 718846661..e4637e1f0 100644
--- a/numba/core/config.py
+++ b/numba/core/config.py
@@ -150,7 +150,9 @@ class _EnvReloader(object):
         # Whether to always warn about potential uninitialized variables
         # because static controlflow analysis cannot find a definition
         # in one or more of the incoming paths.
-        ALWAYS_WARN_UNINIT_VAR = _readenv("ALWAYS_WARN_UNINIT_VAR", int, 0)
+        ALWAYS_WARN_UNINIT_VAR = _readenv(
+            "NUMBA_ALWAYS_WARN_UNINIT_VAR", int, 0,
+        )
 
         # Debug flag to control compiler debug print
         DEBUG = _readenv("NUMBA_DEBUG", int, 0)
diff --git a/numba/core/ir.py b/numba/core/ir.py
index 591462100..b6da59bfb 100644
--- a/numba/core/ir.py
+++ b/numba/core/ir.py
@@ -543,6 +543,17 @@ class Expr(Inst):
         op = 'make_function'
         return cls(op=op, name=name, code=code, closure=closure, defaults=defaults, loc=loc)
 
+    @classmethod
+    def null(cls, loc):
+        """
+        A node for null value.
+
+        This node is not handled by type inference. It is only added by
+        post-typing passes.
+        """
+        op = 'null'
+        return cls(op=op, loc=loc)
+
     def __repr__(self):
         if self.op == 'call':
             args = ', '.join(str(a) for a in self.args)
diff --git a/numba/core/lowering.py b/numba/core/lowering.py
index 2f605d6a9..bdcbbc80f 100644
--- a/numba/core/lowering.py
+++ b/numba/core/lowering.py
@@ -1251,6 +1251,9 @@ class Lower(BaseLower):
         elif expr.op == "phi":
             raise LoweringError("PHI not stripped")
 
+        elif expr.op == 'null':
+            return self.context.get_constant_null(resty)
+
         elif expr.op in self.context.special_ops:
             res = self.context.special_ops[expr.op](self, expr)
             return res
diff --git a/numba/core/pylowering.py b/numba/core/pylowering.py
index 666375c4e..4a139e310 100644
--- a/numba/core/pylowering.py
+++ b/numba/core/pylowering.py
@@ -401,6 +401,10 @@ class PyLower(BaseLower):
         elif expr.op == 'phi':
             raise LoweringError("PHI not stripped")
 
+        elif expr.op == 'null':
+            # Make null value
+            return cgutils.get_null_value(self.pyapi.pyobj)
+
         else:
             raise NotImplementedError(expr)
 
diff --git a/numba/core/ssa.py b/numba/core/ssa.py
index 2f29eedfe..d38918f8c 100644
--- a/numba/core/ssa.py
+++ b/numba/core/ssa.py
@@ -74,6 +74,8 @@ def _fix_ssa_vars(blocks, varname, defmap):
     states['cfg'] = cfg = compute_cfg_from_blocks(blocks)
     states['df+'] = _iterated_domfronts(cfg)
     newblocks = _run_block_rewrite(blocks, states, _FixSSAVars())
+    # check for unneeded phi nodes
+    _remove_unneeded_phis(phimap)
     # insert phi nodes
     for label, philist in phimap.items():
         curblk = newblocks[label]
@@ -82,6 +84,36 @@ def _fix_ssa_vars(blocks, varname, defmap):
     return newblocks
 
 
+def _remove_unneeded_phis(phimap):
+    """Remove unneeded PHIs from the phimap
+    """
+    all_phis = []
+    for philist in phimap.values():
+        all_phis.extend(philist)
+    unneeded_phis = set()
+    # Find unneeded PHIs.
+    for phi in all_phis:
+        ivs = phi.value.incoming_values
+        # It's unneeded if the incomings are either undefined or
+        # the PHI node target is itself
+        if all(iv is ir.UNDEFINED or iv == phi.target for iv in ivs):
+            unneeded_phis.add(phi)
+    # Fix up references to unneeded PHIs
+    for phi in all_phis:
+        for unneed in unneeded_phis:
+            if unneed is not phi:
+                # If the unneeded PHI is in the current phi's incoming values
+                if unneed.target in phi.value.incoming_values:
+                    # Replace the unneeded PHI with an UNDEFINED
+                    idx = phi.value.incoming_values.index(unneed.target)
+                    phi.value.incoming_values[idx] = ir.UNDEFINED
+    # Remove unneeded phis
+    for philist in phimap.values():
+        for unneeded in unneeded_phis:
+            if unneeded in philist:
+                philist.remove(unneeded)
+
+
 def _iterated_domfronts(cfg):
     """Compute the iterated dominance frontiers (DF+ in literatures).
 
diff --git a/numba/core/typed_passes.py b/numba/core/typed_passes.py
index 591e8897d..691996c40 100644
--- a/numba/core/typed_passes.py
+++ b/numba/core/typed_passes.py
@@ -763,22 +763,25 @@ class PreLowerStripPhis(FunctionPass):
 
             # insert exporters
             for target, rhs in exporters[label]:
-                if rhs is not ir.UNDEFINED:
-                    assign = ir.Assign(
-                        target=target,
-                        value=rhs,
-                        loc=target.loc
-                    )
-                    # Insert at the earliest possible location; i.e. after the
-                    # last assignment to rhs
-                    assignments = [stmt
-                                   for stmt in newblk.find_insts(ir.Assign)
-                                   if stmt.target == rhs]
-                    if assignments:
-                        last_assignment = assignments[-1]
-                        newblk.insert_after(assign, last_assignment)
-                    else:
-                        newblk.prepend(assign)
+                # If RHS is undefined
+                if rhs is ir.UNDEFINED:
+                    # Put in a NULL initializer
+                    rhs = ir.Expr.null(loc=target.loc)
+
+                assign = ir.Assign(
+                    target=target,
+                    value=rhs,
+                    loc=target.loc
+                )
+                # Insert at the earliest possible location; i.e. after the
+                # last assignment to rhs
+                assignments = [stmt for stmt in newblk.find_insts(ir.Assign)
+                               if stmt.target == rhs]
+                if assignments:
+                    last_assignment = assignments[-1]
+                    newblk.insert_after(assign, last_assignment)
+                else:
+                    newblk.prepend(assign)
 
         func_ir.blocks = newblocks
         return func_ir


Test patch:
diff --git a/numba/tests/test_ssa.py b/numba/tests/test_ssa.py
index 5af589230..b1bb2b33d 100644
--- a/numba/tests/test_ssa.py
+++ b/numba/tests/test_ssa.py
@@ -7,7 +7,7 @@ import logging
 
 import numpy as np
 
-from numba import njit, types
+from numba import njit, jit, types
 from numba.core import errors
 from numba.extending import overload
 from numba.tests.support import TestCase, override_config
@@ -281,3 +281,92 @@ class TestReportedSSAIssues(SSABaseTest):
             return lin[0]
 
         self.check_func(foo, np.zeros((2, 2)))
+
+    def test_issue5482_missing_variable_init(self):
+        # Test error that lowering fails because variable is missing
+        # a definition before use.
+        @njit("(intp, intp, intp)")
+        def foo(x, v, n):
+            for i in range(n):
+                if i == 0:
+                    if i == x:
+                        pass
+                    else:
+                        problematic = v
+                else:
+                    if i == x:
+                        pass
+                    else:
+                        problematic = problematic + v
+            return problematic
+
+    def test_issue5482_objmode_expr_null_lowering(self):
+        # Existing pipelines will not have the Expr.null in objmode.
+        # We have to create a custom pipeline to force a SSA reconstruction
+        # and stripping.
+        from numba.core.compiler import CompilerBase, DefaultPassBuilder
+        from numba.untyped_passes import ReconstructSSA, IRProcessing
+        from numba.typed_passes import PreLowerStripPhis
+
+        class CustomPipeline(CompilerBase):
+            def define_pipelines(self):
+                pm = DefaultPassBuilder.define_objectmode_pipeline(self.state)
+                # Force SSA reconstruction and stripping
+                pm.add_pass_after(ReconstructSSA, IRProcessing)
+                pm.add_pass_after(PreLowerStripPhis, ReconstructSSA)
+                pm.finalize()
+                return [pm]
+
+        @jit("(intp, intp, intp)", looplift=False,
+             pipeline_class=CustomPipeline)
+        def foo(x, v, n):
+            for i in range(n):
+                if i == n:
+                    if i == x:
+                        pass
+                    else:
+                        problematic = v
+                else:
+                    if i == x:
+                        pass
+                    else:
+                        problematic = problematic + v
+            return problematic
+
+    def test_issue5493_unneeded_phi(self):
+        # Test error that unneeded phi is inserted because variable does not
+        # have a dominance definition.
+        data = (np.ones(2), np.ones(2))
+        A = np.ones(1)
+        B = np.ones((1,1))
+
+        def foo(m, n, data):
+            if len(data) == 1:
+                v0 = data[0]
+            else:
+                v0 = data[0]
+                # Unneeded PHI node for `problematic` would be placed here
+                for _ in range(1, len(data)):
+                    v0 += A
+
+            for t in range(1, m):
+                for idx in range(n):
+                    t = B
+
+                    if idx == 0:
+                        if idx == n - 1:
+                            pass
+                        else:
+                            problematic = t
+                    else:
+                        if idx == n - 1:
+                            pass
+                        else:
+                            problematic = problematic + t
+            return problematic
+
+        expect = foo(10, 10, data)
+        res1 = njit(foo)(10, 10, data)
+        res2 = jit(forceobj=True, looplift=False)(foo)(10, 10, data)
+        np.testing.assert_array_equal(expect, res1)
+        np.testing.assert_array_equal(expect, res2)