Repository: numba/numba

Issue:
Type Inference Change between 0.35 and 0.36?
One of our demo examples has been found to not work using ``numba=0.36+`` and ``jit`` appears to have issues inferring the types.

```python
import numpy as np
from numpy.random import randn
import numba
from numba import jit
import math

@jit(nopython=True)
def compute_call_price(β=0.96,
                       μ=0.005,
                       S0=10,
                       h0=0,
                       K=100,
                       n=10,
                       ρ=0.5,
                       ν=0.001,
                       M=1_000_000):
    current_sum = 0.0
    for m in range(M):
        s = np.log(S0)
        h = h0
        for t in range(n):
            s = s + μ + np.exp(h) * randn()
            h = ρ * h + ν * randn()
        current_sum += np.maximum(np.exp(s) - K, 0)
        
    return β**n + current_sum / M
```

then can be called: ``compute_call_price()`` to execute.

it appears that when using ``numba=0.36+`` all of the argument types are not getting correctly inferred (``pyobject``) which I guess is causing ``nopython=True`` to fail during compilation. The results from ``.inspect_types()``:

```
    # --- LINE 11 --- 
    #   β = arg(0, name=β)  :: pyobject
    #   μ = arg(1, name=μ)  :: pyobject
    #   S0 = arg(2, name=S0)  :: pyobject
    #   h0 = arg(3, name=h0)  :: pyobject
    #   K = arg(4, name=K)  :: pyobject
    #   n = arg(5, name=n)  :: pyobject
    #   ρ = arg(6, name=ρ)  :: pyobject
    #   ν = arg(7, name=ν)  :: pyobject
    #   M = arg(8, name=M)  :: pyobject
    #   $const0.1 = const(float, 0.0)  :: pyobject
    #   current_sum = $const0.1  :: pyobject
    #   jump 4
    # label 4
```
compared to the same in ``0.35``

```
    # --- LINE 11 --- 
    #   β = arg(0, name=β)  :: float64
    #   μ = arg(1, name=μ)  :: float64
    #   S0 = arg(2, name=S0)  :: int64
    #   h0 = arg(3, name=h0)  :: int64
    #   K = arg(4, name=K)  :: int64
    #   n = arg(5, name=n)  :: int64
    #   ρ = arg(6, name=ρ)  :: float64
    #   ν = arg(7, name=ν)  :: float64
    #   M = arg(8, name=M)  :: int64
    #   $const0.1 = const(float, 0.0)  :: float64
    #   current_sum = $const0.1  :: float64
    #   jump 4
    # label 4
```

The actual error from the traceback produced is:

```
TypeError: Failed at nopython (inline calls to locally defined closures)
sequence item 0: expected str instance, NoneType found
```


Discussion:
It looks like there's a bug in the inliner.

Resolving patch:
diff --git a/numba/ir_utils.py b/numba/ir_utils.py
--- a/numba/ir_utils.py
+++ b/numba/ir_utils.py
@@ -1359,7 +1359,7 @@ def find_callname(func_ir, expr, typemap=None, definition_finder=get_definition)
     callee_def = definition_finder(func_ir, callee)
     attrs = []
     while True:
-        if isinstance(callee_def, ir.Global):
+        if isinstance(callee_def, (ir.Global, ir.FreeVar)):
             # require(callee_def.value == numpy)
             # these checks support modules like numpy, numpy.random as well as
             # calls like len() and intrinsitcs like assertEquiv
@@ -1374,7 +1374,16 @@ def find_callname(func_ir, expr, typemap=None, definition_finder=get_definition)
             attrs.append(value)
             if hasattr(callee_def.value, '__module__'):
                 mod_name = callee_def.value.__module__
-                attrs.append(mod_name)
+                # it might be a numpy function imported directly
+                if (hasattr(numpy, value)
+                        and callee_def.value == getattr(numpy, value)):
+                    attrs += ['numpy']
+                # it might be a np.random function imported directly
+                elif (hasattr(numpy.random, value)
+                        and callee_def.value == getattr(numpy.random, value)):
+                    attrs += ['random', 'numpy']
+                elif mod_name is not None:
+                    attrs.append(mod_name)
             else:
                 class_name = callee_def.value.__class__.__name__
                 if class_name == 'builtin_function_or_method':


Test patch:
diff --git a/numba/tests/test_parfors.py b/numba/tests/test_parfors.py
--- a/numba/tests/test_parfors.py
+++ b/numba/tests/test_parfors.py
@@ -12,6 +12,7 @@
 import warnings
 from functools import reduce
 import numpy as np
+from numpy.random import randn
 
 import numba
 from numba import unittest_support as unittest
@@ -478,6 +479,22 @@ def test_impl(v1, v2, m1, m2):
             return 4 * np.sum(x**2 + y**2 < 1) / 10
         self.check(test_impl, *self.simple_args)
 
+    @skip_unsupported
+    def test_np_func_direct_import(self):
+        from numpy import ones  # import here becomes FreeVar
+        def test_impl(n):
+            A = ones(n)
+            return A[0]
+        n = 111
+        self.check(test_impl, n)
+
+    @skip_unsupported
+    def test_np_random_func_direct_import(self):
+        def test_impl(n):
+            A = randn(n)
+            return A[0]
+        self.assertTrue(countParfors(test_impl, (types.int64, )) == 1)
+
     @skip_unsupported
     def test_arange(self):
         # test with stop only