Skip to content
21 changes: 21 additions & 0 deletions numba/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Comment thread
sklam marked this conversation as resolved.
Resolved
"""

feedback_details = """
Please report the error message and traceback, along with a minimal reproducer
at: https://github.com/numba/numba/issues/new

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to update this URL too, while there?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this in a future PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've made a task issue for it: #7261

Expand Down
5 changes: 4 additions & 1 deletion numba/core/ssa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
90 changes: 89 additions & 1 deletion numba/tests/test_ir.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -466,5 +484,75 @@ 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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment explaining why this dummy is needed: multiple assignments to the same variable name are needed to trigger SSA.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 3d6fb07

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()