Repository: numba/numba

Issue:
Regression: SSA scope check assertion error
If anyone see `AssertionError` from:

https://github.com/numba/numba/blob/master/numba/core/ssa.py#L270

please report to this issue with reproducers.


Discussion:
I managed to build a reproducer, probably not as minimal as it can be but I can confirm that they key bit is variable `i`. Removing `i += 1` makes the problem go away.

```python
from collections import namedtuple
import numpy as np
from numba.extending import overload_method
from numba.typed import List
from numba import njit, literal_unroll, typeof, types

first = List([np.array([(1, 2)], dtype=[('val', 'f8'), ('b', 'i8')])])
second = List([np.array([(1, 2)], dtype=[('val', 'f8'), ('d', 'i8')])])

A = namedtuple("A", ["first", "second"])

a = A(first, second)


@overload_method(types.NamedTuple, 'bar')
def bar_ovrl(a):

    def bar_impl(a):
        i = 0
        b = 0
        for x in literal_unroll((a.first, a.second)):
            for i, y in enumerate(x):
                for z in y:
                    b += z['val']
                    i += 1
        return b

    return bar_impl


@njit
def foo(a):
    return a.bar()

print(foo(a))
```

@luk-f-a, thank you for finding a reproducer, it makes it a lot easier for us to debug.

Resolving patch:
diff --git a/numba/core/errors.py b/numba/core/errors.py
--- a/numba/core/errors.py
+++ b/numba/core/errors.py
@@ -82,6 +82,20 @@ class NumbaInvalidConfigWarning(NumbaWarning):
     Warning category for using an invalid configuration.
     """
 
+
+class NumbaPedanticWarning(NumbaWarning):
+    """
+    Warning category for reporting pedantic messages.
+    """
+    def __init__(self, msg, **kwargs):
+        super().__init__(f"{msg}\n{pedantic_warning_info}")
+
+
+class NumbaIRAssumptionWarning(NumbaPedanticWarning):
+    """
+    Warning category for reporting an IR assumption violation.
+    """
+
 # These are needed in the color formatting of errors setup
 
 
@@ -314,6 +328,13 @@ def termcolor():
             _termcolor_inst = HighlightColorScheme(scheme)
         return _termcolor_inst
 
+
+pedantic_warning_info = """
+This warning came from an internal pedantic check. Please report the warning
+message and traceback, along with a minimal reproducer at:
+https://github.com/numba/numba/issues/new?template=bug_report.md
+"""
+
 feedback_details = """
 Please report the error message and traceback, along with a minimal reproducer
 at: https://github.com/numba/numba/issues/new
diff --git a/numba/core/ssa.py b/numba/core/ssa.py
--- a/numba/core/ssa.py
+++ b/numba/core/ssa.py
@@ -267,7 +267,10 @@ def on_assign(self, states, assign):
             if len(defmap) == 0:
                 newtarget = assign.target
                 _logger.debug("first assign: %s", newtarget)
-                assert newtarget.name in scope.localvars
+                if newtarget.name not in scope.localvars:
+                    wmsg = f"variable {newtarget.name!r} is not in scope."
+                    warnings.warn(errors.NumbaIRAssumptionWarning(wmsg,
+                                  loc=assign.loc))
             else:
                 newtarget = scope.redefine(assign.target.name, loc=assign.loc)
             assign = ir.Assign(


Test patch:
diff --git a/numba/tests/test_ir.py b/numba/tests/test_ir.py
--- a/numba/tests/test_ir.py
+++ b/numba/tests/test_ir.py
@@ -1,7 +1,25 @@
 import unittest
-from numba import objmode
+from unittest.case import TestCase
+import warnings
 import numpy as np
+
+from numba import objmode
 from numba.core import ir, compiler
+from numba.core import errors
+from numba.core.compiler import (
+    CompilerBase,
+    ReconstructSSA,
+)
+from numba.core.compiler_machinery import (
+    FunctionPass,
+    PassManager,
+    register_pass,
+)
+from numba.core.untyped_passes import (
+    TranslateByteCode,
+    IRProcessing,
+)
+from numba import njit
 
 
 class TestIR(unittest.TestCase):
@@ -466,5 +484,77 @@ def baz(a, b):
         check_diffstr(tmp, ["c + b", "b + c"])
 
 
+class TestIRPedanticChecks(TestCase):
+    def test_var_in_scope_assumption(self):
+        # Create a pass that clears ir.Scope in ir.Block
+        @register_pass(mutates_CFG=False, analysis_only=False)
+        class RemoveVarInScope(FunctionPass):
+            _name = "_remove_var_in_scope"
+
+            def __init__(self):
+                FunctionPass.__init__(self)
+
+            # implement method to do the work, "state" is the internal compiler
+            # state from the CompilerBase instance.
+            def run_pass(self, state):
+                func_ir = state.func_ir
+                # walk the blocks
+                for blk in func_ir.blocks.values():
+                    oldscope = blk.scope
+                    # put in an empty Scope
+                    blk.scope = ir.Scope(parent=oldscope.parent,
+                                         loc=oldscope.loc)
+                return True
+
+        # Create a pass that always fails, to stop the compiler
+        @register_pass(mutates_CFG=False, analysis_only=False)
+        class FailPass(FunctionPass):
+            _name = "_fail"
+
+            def __init__(self, *args, **kwargs):
+                FunctionPass.__init__(self)
+
+            def run_pass(self, state):
+                # This is unreachable. SSA pass should have raised before this
+                # pass when run with `error.NumbaPedanticWarning`s raised as
+                # errors.
+                raise AssertionError("unreachable")
+
+        class MyCompiler(CompilerBase):
+            def define_pipelines(self):
+                pm = PassManager("testing pm")
+                pm.add_pass(TranslateByteCode, "analyzing bytecode")
+                pm.add_pass(IRProcessing, "processing IR")
+                pm.add_pass(RemoveVarInScope, "_remove_var_in_scope")
+                pm.add_pass(ReconstructSSA, "ssa")
+                pm.add_pass(FailPass, "_fail")
+                pm.finalize()
+                return [pm]
+
+        @njit(pipeline_class=MyCompiler)
+        def dummy(x):
+            # To trigger SSA and the pedantic check, this function must have
+            # multiple assignments to the same variable in different blocks.
+            a = 1
+            b = 2
+            if a < b:
+                a = 2
+            else:
+                b = 3
+            return a, b
+
+        with warnings.catch_warnings():
+            # Make NumbaPedanticWarning an error
+            warnings.simplefilter("error", errors.NumbaPedanticWarning)
+            # Catch NumbaIRAssumptionWarning
+            with self.assertRaises(errors.NumbaIRAssumptionWarning) as raises:
+                dummy(1)
+            # Verify the error message
+            self.assertRegex(
+                str(raises.exception),
+                r"variable '[a-z]' is not in scope",
+            )
+
+
 if __name__ == '__main__':
     unittest.main()