Repository: numba/numba

Issue:
Function working with 0.46 doesn't work with 0.47 (runs indefinitely)
## Reporting a bug

- [x] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

I have a function called `_compute_shapelet_grad` that works perfectly fines using `llvmlite==0.30` and `numba==0.46` but runs indefinitely using `llvmlite==0.31` and `numba==0.47`.

## MWE

### Functions

```python
from numba import njit, prange
import numpy as np
from numpy.lib.stride_tricks import as_strided

@njit()
def _windowed_view(X, n_samples, n_timestamps, window_size, window_step):
    overlap = window_size - window_step
    shape_new = (n_samples,
                 (n_timestamps - overlap) // window_step,
                 window_size // 1)
    s0, s1 = X.strides
    strides_new = (s0, window_step * s1, s1)
    return as_strided(X, shape=shape_new, strides=strides_new)


@njit(parallel=True, fastmath=True)
def _softmin(arr, alpha):
    """Derive the soft-minimum of an array."""
    maximum = np.max(alpha * arr)
    exp = np.exp(alpha * arr - maximum)
    num = np.sum(arr * exp)
    den = np.sum(exp)
    return num / den


@njit(parallel=True, fastmath=True)
def _softmin_grad(arr, alpha):
    """Derive the gradient of the softmin function."""
    minimum = _softmin(arr, alpha)
    maximum = np.max(alpha * arr)
    exp = np.exp(alpha * arr - maximum)
    num = exp * (1 + alpha * (arr - minimum))
    den = np.sum(exp)
    return num / den

@njit()
def _compute_shapelet_grad(
    X, n_samples, n_timestamps, weights, shapelets, lengths, alpha,
    proba_minus_y, weight_idx, sample_weight,
):

    gradients = []

    for i in range(len(lengths)):
        X_window = _windowed_view(X, n_samples, n_timestamps,
                                  window_size=lengths[i][0], window_step=1)
        n_windows = X_window.shape[1]
        size = shapelets[i][0].size
        for j in range(shapelets[i].shape[0]):
            # Get the current shapelet
            shapelet = shapelets[i][j]

            # Derive the difference and the distance
            diff = shapelet - X_window
            dist = np.empty((n_samples, n_windows))
            for k in prange(n_samples):
                for l in prange(n_windows):
                    dist[k, l] = np.mean(diff[k, l] ** 2)

            # Derive the softmin gradient
            softmin_gradient = np.empty((n_samples, n_windows))
            for k in prange(n_samples):
                softmin_gradient[k] = _softmin_grad(dist[k], alpha)

            # Normalize the difference for actual gradient
            diff *= (2 / size)

            # Compute the gradient
            grad = np.empty(size)
            if weights.ndim == 1:
                for k in prange(size):
                    grad[k] = np.mean(
                        np.sum(diff[:, :, k] * softmin_gradient, axis=1) *
                        weights[weight_idx] *
                        proba_minus_y *
                        sample_weight
                    )
            else:
                for k in prange(size):
                    grad[k] = np.mean(
                        np.sum(diff[:, :, k] * softmin_gradient, axis=1) *
                        np.sum(weights[weight_idx] * proba_minus_y, axis=1) *
                        sample_weight
                    )
            gradients.append(grad)

            # Update the weight index
            weight_idx += 1

    return gradients
```

### Example

```python
X = np.arange(10, dtype='float64').reshape(2, 5)
n_samples, n_timestamps = X.shape
proba_minus_y = np.array([0.1, 0.9]) - np.arange(2)
n_classes = 2
shapelets = (np.array([[4., 6.]]), np.array([[0., 1., 3., 3.]]))
lengths = (np.array([2]), np.array([4]))
alpha = -100
weights = np.zeros(2)
fit_intercept = False
penalty = 'l2'
C = 1
sample_weight = np.array([1., 1.]).reshape(-1, 1)
weight_idx = 1

_compute_shapelet_grad(
    X, n_samples, n_timestamps, weights, shapelets, lengths, alpha,
    proba_minus_y, weight_idx, sample_weight,
)
```

Interesting fact: if I don't define the `_windowed_view`, `_softmin ` and `_softmin_grad` functions, no error is raised, so it must be stuck somewhere before these checks occur.
Function working with 0.46 doesn't work with 0.47 (runs indefinitely)
## Reporting a bug

- [x] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

I have a function called `_compute_shapelet_grad` that works perfectly fines using `llvmlite==0.30` and `numba==0.46` but runs indefinitely using `llvmlite==0.31` and `numba==0.47`.

## MWE

### Functions

```python
from numba import njit, prange
import numpy as np
from numpy.lib.stride_tricks import as_strided

@njit()
def _windowed_view(X, n_samples, n_timestamps, window_size, window_step):
    overlap = window_size - window_step
    shape_new = (n_samples,
                 (n_timestamps - overlap) // window_step,
                 window_size // 1)
    s0, s1 = X.strides
    strides_new = (s0, window_step * s1, s1)
    return as_strided(X, shape=shape_new, strides=strides_new)


@njit(parallel=True, fastmath=True)
def _softmin(arr, alpha):
    """Derive the soft-minimum of an array."""
    maximum = np.max(alpha * arr)
    exp = np.exp(alpha * arr - maximum)
    num = np.sum(arr * exp)
    den = np.sum(exp)
    return num / den


@njit(parallel=True, fastmath=True)
def _softmin_grad(arr, alpha):
    """Derive the gradient of the softmin function."""
    minimum = _softmin(arr, alpha)
    maximum = np.max(alpha * arr)
    exp = np.exp(alpha * arr - maximum)
    num = exp * (1 + alpha * (arr - minimum))
    den = np.sum(exp)
    return num / den

@njit()
def _compute_shapelet_grad(
    X, n_samples, n_timestamps, weights, shapelets, lengths, alpha,
    proba_minus_y, weight_idx, sample_weight,
):

    gradients = []

    for i in range(len(lengths)):
        X_window = _windowed_view(X, n_samples, n_timestamps,
                                  window_size=lengths[i][0], window_step=1)
        n_windows = X_window.shape[1]
        size = shapelets[i][0].size
        for j in range(shapelets[i].shape[0]):
            # Get the current shapelet
            shapelet = shapelets[i][j]

            # Derive the difference and the distance
            diff = shapelet - X_window
            dist = np.empty((n_samples, n_windows))
            for k in prange(n_samples):
                for l in prange(n_windows):
                    dist[k, l] = np.mean(diff[k, l] ** 2)

            # Derive the softmin gradient
            softmin_gradient = np.empty((n_samples, n_windows))
            for k in prange(n_samples):
                softmin_gradient[k] = _softmin_grad(dist[k], alpha)

            # Normalize the difference for actual gradient
            diff *= (2 / size)

            # Compute the gradient
            grad = np.empty(size)
            if weights.ndim == 1:
                for k in prange(size):
                    grad[k] = np.mean(
                        np.sum(diff[:, :, k] * softmin_gradient, axis=1) *
                        weights[weight_idx] *
                        proba_minus_y *
                        sample_weight
                    )
            else:
                for k in prange(size):
                    grad[k] = np.mean(
                        np.sum(diff[:, :, k] * softmin_gradient, axis=1) *
                        np.sum(weights[weight_idx] * proba_minus_y, axis=1) *
                        sample_weight
                    )
            gradients.append(grad)

            # Update the weight index
            weight_idx += 1

    return gradients
```

### Example

```python
X = np.arange(10, dtype='float64').reshape(2, 5)
n_samples, n_timestamps = X.shape
proba_minus_y = np.array([0.1, 0.9]) - np.arange(2)
n_classes = 2
shapelets = (np.array([[4., 6.]]), np.array([[0., 1., 3., 3.]]))
lengths = (np.array([2]), np.array([4]))
alpha = -100
weights = np.zeros(2)
fit_intercept = False
penalty = 'l2'
C = 1
sample_weight = np.array([1., 1.]).reshape(-1, 1)
weight_idx = 1

_compute_shapelet_grad(
    X, n_samples, n_timestamps, weights, shapelets, lengths, alpha,
    proba_minus_y, weight_idx, sample_weight,
)
```

Interesting fact: if I don't define the `_windowed_view`, `_softmin ` and `_softmin_grad` functions, no error is raised, so it must be stuck somewhere before these checks occur.


Discussion:
Thanks for the report. Looks like `byteflow.py` is stuck in a loop. CC @sklam.
i'm on it
Thanks for the report. Looks like `byteflow.py` is stuck in a loop. CC @sklam.
i'm on it

Resolving patch:
diff --git a/numba/byteflow.py b/numba/byteflow.py
--- a/numba/byteflow.py
+++ b/numba/byteflow.py
@@ -194,15 +194,20 @@ def propagate_phi_map(phismap):
             """An iterative dataflow algorithm to find the definition
             (the source) of each PHI node.
             """
+            blacklist = defaultdict(set)
+
             while True:
                 changing = False
                 for phi, defsites in sorted(list(phismap.items())):
                     for rhs, state in sorted(list(defsites)):
                         if rhs in phi_set:
-                            old = frozenset(defsites)
                             defsites |= phismap[rhs]
-                            defsites.discard((rhs, state))
-                            changing = old != defsites
+                            blacklist[phi].add((rhs, state))
+                    to_remove = blacklist[phi]
+                    if to_remove & defsites:
+                        defsites -= to_remove
+                        changing = True
+
                 _logger.debug("changing phismap: %s", _lazy_pformat(phismap))
                 if not changing:
                     break
@@ -1026,6 +1031,7 @@ def op_CALL_METHOD(self, state, inst):
         self.op_CALL_FUNCTION(state, inst)
 
 
+@total_ordering
 class State(object):
     """State of the trace
     """
@@ -1072,6 +1078,9 @@ def get_identity(self):
     def __hash__(self):
         return hash(self.get_identity())
 
+    def __lt__(self, other):
+        return self.get_identity() < other.get_identity()
+
     def __eq__(self, other):
         return self.get_identity() == other.get_identity()
 


Test patch:
diff --git a/numba/tests/test_byteflow.py b/numba/tests/test_byteflow.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_byteflow.py
@@ -0,0 +1,67 @@
+"""
+Test byteflow.py specific issues
+"""
+from numba.tests.support import TestCase
+from numba.compiler import run_frontend
+
+
+class TestByteFlowIssues(TestCase):
+    def test_issue_5087(self):
+        # This is an odd issue. The exact number of print below is
+        # necessary to trigger it. Too many or too few will alter the behavior.
+        # Also note that the function below will not be executed. The problem
+        # occurs at compilation. The definition below is invalid for execution.
+        # The problem occurs in the bytecode analysis.
+        def udt():
+            print
+            print
+            print
+
+            for i in range:
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+                print
+
+                for j in range:
+                    print
+                    print
+                    print
+                    print
+                    print
+                    print
+                    print
+                    for k in range:
+                        for l in range:
+                            print
+
+                    print
+                    print
+                    print
+                    print
+                    print
+                    print
+                    print
+                    print
+                    print
+                    if print:
+                        for n in range:
+                            print
+                    else:
+                        print
+
+        run_frontend(udt)