Repository: numba/numba

Issue:
Wrongly evaluated equality comparison
## Reporting a bug

- [x] I have tried using the latest released version of Numba: yes, using version 0.54.1 (7 October, 2021)
- [x] I have included a self contained code sample to reproduce the problem.

**`bug.py`**

```python
from numba import njit


@njit
def func(x: float):
    print("x:", x)

    if x > 1:
        action = "a"
    elif x < -1:
        action = "b"
    else:
        action = None

    print("action:", action, action == "a", action == "b")

    if action == "a":
        print("performing action a")
    elif action == "b":
        print("performing action b")


func(1.2)
```

**Output**

```
x: 1.2
action: a False False
```

Apparently the condition `action == "a"` is wrongly evaluated.

**Expected Output**

This is the output when running with plain Python, i.e., removing `@njit`:

```
x: 1.2
action: a True False
performing action a
```


Discussion:
Or perhaps even more simplified.

**`bug.py`**

```python
from numba import njit


@njit
def func(x: float):
    print("x:", x)

    action = None
    if x > 1:
        action = "a"

    if action == "a":
        print("yes")


func(1.2)
```
Hi,
Happens to me also.

```python
def func_1(x: float):
    action = None
    if x > 1:
        action = "a"
    else:
        action = "b"

    print(action)

    if action == "a":
        print("func_1: yes")  # get printed


@profile
def func_1_wrapper():
    _func_1_jit = jit(nopython=True)(func_1)
    _func_1_jit(1.2)


def func_2(x: float):
    action = None
    if x > 1:
        action = "a"

    print(action)  # prints "a"

    if action == "a":
        print("func_2: yes")  # doesnt get printed.

@profile
def func_2_wrapper():
    _func_2_jit = jit(nopython=True)(func_2)
    _func_2_jit(1.2)

if __name__ == '__main__':

    func_1_wrapper()  # print "yes"
    func_2_wrapper()  # doesnt print "yes"

```

I also tried to use cProfile to get more info.
for "func_1" : 1285522 function calls (1208653 primitive calls) in 1.802 seconds
for "func_2": 139373 function calls (131296 primitive calls) in 0.287 seconds

full profiling is here: 
https://github.com/LiorA1/PythonNumba/tree/main/if_cond_profiling

Hope that it will be useful..
Edit: Tested in Numba 0.54.1 / 0.53.1 / 0.50.1
@bluenote10 thank you for reporting this. It seems like this is the result of Numba being sensitive to typing. It is either because the final `else` clause assigns a `None` or because the predicate compares integer against floating point. Here is a suitable workaround:

```python
from numba import njit


@njit
def func(x: float):
    print("x:", x)

    if x > 1.0:
        action = "a"
    elif x < -1.0:
        action = "b"

    print("action:", action, action == "a", action == "b")

    if action == "a":
        print("performing action a")
    elif action == "b":
        print("performing action b")


func(1.2)

```
I looked at the reproducer some more. I think the problem is the `else`. I reverted the values in the predicate back to integers and it was fine. I think `action` is being typed as an `optional` type, which is either of `unicode` type or `None` and I think that the comparison operator for `optional` types is buggy. I think there may even be a reported issue already, so it's probably worth looking through the issue tracker for any duplicates.
Thanks for the report. The issue is that `action` is typed as a `types.Optional`, which evades the logic in `operator.eq` for the `UnicodeType`-like types.

Something like this should fix:
```diff
diff --git a/numba/cpython/unicode.py b/numba/cpython/unicode.py
index 3cad38b..131ff5a 100644
--- a/numba/cpython/unicode.py
+++ b/numba/cpython/unicode.py
@@ -451,11 +451,26 @@ def unicode_len(s):
 def unicode_eq(a, b):
     if not (a.is_internal and b.is_internal):
         return
+    if isinstance(a, types.Optional):
+        check_a = a.type
+    else:
+        check_a = a
+    if isinstance(b, types.Optional):
+        check_b = b.type
+    else:
+        check_b = b
     accept = (types.UnicodeType, types.StringLiteral, types.UnicodeCharSeq)
-    a_unicode = isinstance(a, accept)
-    b_unicode = isinstance(b, accept)
+    a_unicode = isinstance(check_a, accept)
+    b_unicode = isinstance(check_b, accept)
     if a_unicode and b_unicode:
         def eq_impl(a, b):
+            a_none = a is None
+            b_none = b is None
+            if a_none or b_none:
+                if a_none and b_none:
+                    return True
+                else:
+                    return False
             # the str() is for UnicodeCharSeq, it's a nop else
             a = str(a)
             b = str(b)
```

Resolving patch:
diff --git a/numba/cpython/unicode.py b/numba/cpython/unicode.py
--- a/numba/cpython/unicode.py
+++ b/numba/cpython/unicode.py
@@ -451,11 +451,27 @@ def len_impl(s):
 def unicode_eq(a, b):
     if not (a.is_internal and b.is_internal):
         return
+    if isinstance(a, types.Optional):
+        check_a = a.type
+    else:
+        check_a = a
+    if isinstance(b, types.Optional):
+        check_b = b.type
+    else:
+        check_b = b
     accept = (types.UnicodeType, types.StringLiteral, types.UnicodeCharSeq)
-    a_unicode = isinstance(a, accept)
-    b_unicode = isinstance(b, accept)
+    a_unicode = isinstance(check_a, accept)
+    b_unicode = isinstance(check_b, accept)
     if a_unicode and b_unicode:
         def eq_impl(a, b):
+            # handle Optionals at runtime
+            a_none = a is None
+            b_none = b is None
+            if a_none or b_none:
+                if a_none and b_none:
+                    return True
+                else:
+                    return False
             # the str() is for UnicodeCharSeq, it's a nop else
             a = str(a)
             b = str(b)


Test patch:
diff --git a/numba/tests/test_unicode.py b/numba/tests/test_unicode.py
--- a/numba/tests/test_unicode.py
+++ b/numba/tests/test_unicode.py
@@ -446,6 +446,28 @@ def test_eq(self, flags=no_pyobj_flags):
                 self.assertEqual(pyfunc(1, b),
                                  cfunc(1, b), '%s, %s' % (1, b))
 
+    def test_eq_optional(self):
+        # See issue #7474
+        @njit
+        def foo(pred1, pred2):
+            if pred1 > 0:
+                resolved1 = 'concrete'
+            else:
+                resolved1 = None
+            if pred2 < 0:
+                resolved2 = 'concrete'
+            else:
+                resolved2 = None
+
+            # resolved* are Optionals
+            if resolved1 == resolved2:
+                return 10
+            else:
+                return 20
+
+        for (p1, p2) in product(*((-1, 1),) * 2):
+            self.assertEqual(foo(p1, p2), foo.py_func(p1, p2))
+
     def _check_ordering_op(self, usecase):
         pyfunc = usecase
         cfunc = njit(pyfunc)