Repository: numba/numba

Issue:
Inplace operator error for arrays
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I am using the latest ~~released~~ master 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).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->
Inplace operators such as `operator.iadd` fail on arrays when used explicitly. Using them explicitly is helpful in general since it allows writing generic code more easily.

```python
import operator

@numba.njit
def f(A):
    return operator.iadd(A, A)
f(np.array([1,3]))
```
```
TypeError                                 Traceback (most recent call last)
~/dev/numba/numba/errors.py in new_error_context(fmt_, *args, **kwargs)
    660     try:
--> 661         yield
    662     except NumbaError as e:

~/dev/numba/numba/lowering.py in lower_block(self, block)
    257                                    loc=self.loc, errcls_=defaulterrcls):
--> 258                 self.lower_inst(inst)
    259 

~/dev/numba/numba/lowering.py in lower_inst(self, inst)
    300             ty = self.typeof(inst.target.name)
--> 301             val = self.lower_assign(ty, inst)
    302             self.storevar(val, inst.target.name)

~/dev/numba/numba/lowering.py in lower_assign(self, ty, inst)
    458         elif isinstance(value, ir.Expr):
--> 459             return self.lower_expr(ty, value)
    460 

~/dev/numba/numba/lowering.py in lower_expr(self, resty, expr)
    917         elif expr.op == 'call':
--> 918             res = self.lower_call(resty, expr)
    919             return res

~/dev/numba/numba/lowering.py in lower_call(self, resty, expr)
    710         else:
--> 711             res = self._lower_call_normal(fnty, expr, signature)
    712 

~/dev/numba/numba/lowering.py in _lower_call_normal(self, fnty, expr, signature)
    889 
--> 890         res = impl(self.builder, argvals, self.loc)
    891         return res

~/dev/numba/numba/targets/base.py in __call__(self, builder, args, loc)
   1131     def __call__(self, builder, args, loc=None):
-> 1132         res = self._imp(self._context, builder, self._sig, args, loc=loc)
   1133         self._context.add_linking_libs(getattr(self, 'libs', ()))

~/dev/numba/numba/targets/base.py in wrapper(*args, **kwargs)
   1156             kwargs.pop('loc')     # drop unused loc
-> 1157             return fn(*args, **kwargs)
   1158 

~/dev/numba/numba/targets/npyimpl.py in lower_inplace_operator(context, builder, sig, args)
    497         # is (A, B, A) -> A
--> 498         args = args + (args[0],)
    499         sig = typing.signature(sig.return_type, *sig.args + (sig.args[0],))

TypeError: can only concatenate list (not "tuple") to list

During handling of the above exception, another exception occurred:

LoweringError                             Traceback (most recent call last)
<ipython-input-5-a36eda57cdc1> in <module>
----> 1 f(A)

~/dev/numba/numba/dispatcher.py in _compile_for_args(self, *args, **kws)
    368                     e.patch_message(''.join(e.args) + help_msg)
    369             # ignore the FULL_TRACEBACKS config, this needs reporting!
--> 370             raise e
    371 
    372     def inspect_llvm(self, signature=None):

~/dev/numba/numba/dispatcher.py in _compile_for_args(self, *args, **kws)
    325                 argtypes.append(self.typeof_pyval(a))
    326         try:
--> 327             return self.compile(tuple(argtypes))
    328         except errors.TypingError as e:
    329             # Intercept typing error that may be due to an argument

~/dev/numba/numba/compiler_lock.py in _acquire_compile_lock(*args, **kwargs)
     30         def _acquire_compile_lock(*args, **kwargs):
     31             with self:
---> 32                 return func(*args, **kwargs)
     33         return _acquire_compile_lock
     34 

~/dev/numba/numba/dispatcher.py in compile(self, sig)
    657 
    658             self._cache_misses[sig] += 1
--> 659             cres = self._compiler.compile(args, return_type)
    660             self.add_overload(cres)
    661             self._cache.save_overload(sig, cres)

~/dev/numba/numba/dispatcher.py in compile(self, args, return_type)
     81                                       args=args, return_type=return_type,
     82                                       flags=flags, locals=self.locals,
---> 83                                       pipeline_class=self.pipeline_class)
     84         # Check typing error if object mode is used
     85         if cres.typing_error is not None and not flags.enable_pyobject:

~/dev/numba/numba/compiler.py in compile_extra(typingctx, targetctx, func, args, return_type, flags, locals, library, pipeline_class)
    953     pipeline = pipeline_class(typingctx, targetctx, library,
    954                               args, return_type, flags, locals)
--> 955     return pipeline.compile_extra(func)
    956 
    957 

~/dev/numba/numba/compiler.py in compile_extra(self, func)
    375         self.lifted = ()
    376         self.lifted_from = None
--> 377         return self._compile_bytecode()
    378 
    379     def compile_ir(self, func_ir, lifted=(), lifted_from=None):

~/dev/numba/numba/compiler.py in _compile_bytecode(self)
    884         """
    885         assert self.func_ir is None
--> 886         return self._compile_core()
    887 
    888     def _compile_ir(self):

~/dev/numba/numba/compiler.py in _compile_core(self)
    871         self.define_pipelines(pm)
    872         pm.finalize()
--> 873         res = pm.run(self.status)
    874         if res is not None:
    875             # Early pipeline completion

~/dev/numba/numba/compiler_lock.py in _acquire_compile_lock(*args, **kwargs)
     30         def _acquire_compile_lock(*args, **kwargs):
     31             with self:
---> 32                 return func(*args, **kwargs)
     33         return _acquire_compile_lock
     34 

~/dev/numba/numba/compiler.py in run(self, status)
    252                     # No more fallback pipelines?
    253                     if is_final_pipeline:
--> 254                         raise patched_exception
    255                     # Go to next fallback pipeline
    256                     else:

~/dev/numba/numba/compiler.py in run(self, status)
    243                 try:
    244                     event("-- %s" % stage_name)
--> 245                     stage()
    246                 except _EarlyPipelineCompletion as e:
    247                     return e.result

~/dev/numba/numba/compiler.py in stage_nopython_backend(self)
    745         """
    746         lowerfn = self.backend_nopython_mode
--> 747         self._backend(lowerfn, objectmode=False)
    748 
    749     def stage_compile_interp_mode(self):

~/dev/numba/numba/compiler.py in _backend(self, lowerfn, objectmode)
    685             self.library.enable_object_caching()
    686 
--> 687         lowered = lowerfn()
    688         signature = typing.signature(self.return_type, *self.args)
    689         self.cr = compile_result(

~/dev/numba/numba/compiler.py in backend_nopython_mode(self)
    672                 self.calltypes,
    673                 self.flags,
--> 674                 self.metadata)
    675 
    676     def _backend(self, lowerfn, objectmode):

~/dev/numba/numba/compiler.py in native_lowering_stage(targetctx, library, interp, typemap, restype, calltypes, flags, metadata)
   1122         lower = lowering.Lower(targetctx, library, fndesc, interp,
   1123                                metadata=metadata)
-> 1124         lower.lower()
   1125         if not flags.no_cpython_wrapper:
   1126             lower.create_cpython_wrapper(flags.release_gil)

~/dev/numba/numba/lowering.py in lower(self)
    175         if self.generator_info is None:
    176             self.genlower = None
--> 177             self.lower_normal_function(self.fndesc)
    178         else:
    179             self.genlower = self.GeneratorLower(self)

~/dev/numba/numba/lowering.py in lower_normal_function(self, fndesc)
    216         # Init argument values
    217         self.extract_function_arguments()
--> 218         entry_block_tail = self.lower_function_body()
    219 
    220         # Close tail of entry block

~/dev/numba/numba/lowering.py in lower_function_body(self)
    241             bb = self.blkmap[offset]
    242             self.builder.position_at_end(bb)
--> 243             self.lower_block(block)
    244 
    245         self.post_lower()

~/dev/numba/numba/lowering.py in lower_block(self, block)
    256             with new_error_context('lowering "{inst}" at {loc}', inst=inst,
    257                                    loc=self.loc, errcls_=defaulterrcls):
--> 258                 self.lower_inst(inst)
    259 
    260     def create_cpython_wrapper(self, release_gil=False):

~/dev/miniconda3/envs/DEV/lib/python3.7/contextlib.py in __exit__(self, type, value, traceback)
    128                 value = type()
    129             try:
--> 130                 self.gen.throw(type, value, traceback)
    131             except StopIteration as exc:
    132                 # Suppress StopIteration *unless* it's the same exception that

~/dev/numba/numba/errors.py in new_error_context(fmt_, *args, **kwargs)
    667         from numba import config
    668         tb = sys.exc_info()[2] if config.FULL_TRACEBACKS else None
--> 669         six.reraise(type(newerr), newerr, tb)
    670 
    671 

~/dev/numba/numba/six.py in reraise(tp, value, tb)
    657         if value.__traceback__ is not tb:
    658             raise value.with_traceback(tb)
--> 659         raise value
    660 
    661 else:

LoweringError: Failed in nopython mode pipeline (step: nopython mode backend)
can only concatenate list (not "tuple") to list

File "<ipython-input-4-a98347f692b7>", line 3:
def f(A):
    return operator.iadd(A, A)
    ^

[1] During: lowering "$0.5 = call $0.2(A, A, func=$0.2, args=[Var(A, <ipython-input-4-a98347f692b7> (3)), Var(A, <ipython-input-4-a98347f692b7> (3))], kws=(), vararg=None)" at <ipython-input-4-a98347f692b7> (3)

```


Discussion:
it's caused by a 1-line bug

Resolving patch:
diff --git a/numba/targets/npyimpl.py b/numba/targets/npyimpl.py
--- a/numba/targets/npyimpl.py
+++ b/numba/targets/npyimpl.py
@@ -495,7 +495,7 @@ def lower_inplace_operator(context, builder, sig, args):
         # The visible signature is (A, B) -> A
         # The implementation's signature (with explicit output)
         # is (A, B, A) -> A
-        args = args + (args[0],)
+        args = tuple(args) + (args[0],)
         sig = typing.signature(sig.return_type, *sig.args + (sig.args[0],))
         return numpy_ufunc_kernel(context, builder, sig, args, kernel,
                                   explicit_output=True)


Test patch:
diff --git a/numba/tests/test_ufuncs.py b/numba/tests/test_ufuncs.py
--- a/numba/tests/test_ufuncs.py
+++ b/numba/tests/test_ufuncs.py
@@ -6,6 +6,7 @@
 import sys
 import warnings
 import threading
+import operator
 
 import numpy as np
 
@@ -17,7 +18,7 @@
 from numba.config import PYVERSION
 from numba.errors import LoweringError, TypingError
 from .support import TestCase, CompilationCache, MemoryLeakMixin, tag
-from numba.six import exec_
+from numba.six import exec_, string_types
 from numba.typing.npydecl import supported_ufuncs, all_ufuncs
 
 is32bits = tuple.__itemsize__ == 4
@@ -95,10 +96,19 @@ def _make_binary_ufunc_op_usecase(ufunc_op):
 
 
 def _make_inplace_ufunc_op_usecase(ufunc_op):
-    ldict = {}
-    exec_("def fn(x,y):\n    x{0}y".format(ufunc_op), globals(), ldict)
-    fn = ldict["fn"]
-    fn.__name__ = "usecase_{0}".format(hash(ufunc_op))
+    """Generates a function to be compiled that performs an inplace operation
+
+    ufunc_op can be a string like '+=' or a function like operator.iadd
+    """
+    if isinstance(ufunc_op, string_types):
+        ldict = {}
+        exec_("def fn(x,y):\n    x{0}y".format(ufunc_op), globals(), ldict)
+        fn = ldict["fn"]
+        fn.__name__ = "usecase_{0}".format(hash(ufunc_op))
+    else:
+        def inplace_op(x, y):
+            ufunc_op(x, y)
+        fn = inplace_op
     return fn
 
 
@@ -1074,43 +1084,55 @@ def test_unary_invert_array_op(self):
     @tag('important')
     def test_inplace_add(self):
         self.inplace_float_op_test('+=', [-1, 1.5, 3], [-5, 0, 2.5])
+        self.inplace_float_op_test(operator.iadd, [-1, 1.5, 3], [-5, 0, 2.5])
 
     @tag('important')
     def test_inplace_sub(self):
         self.inplace_float_op_test('-=', [-1, 1.5, 3], [-5, 0, 2.5])
+        self.inplace_float_op_test(operator.isub, [-1, 1.5, 3], [-5, 0, 2.5])
 
     @tag('important')
     def test_inplace_mul(self):
         self.inplace_float_op_test('*=', [-1, 1.5, 3], [-5, 0, 2.5])
+        self.inplace_float_op_test(operator.imul, [-1, 1.5, 3], [-5, 0, 2.5])
 
     def test_inplace_floordiv(self):
         self.inplace_float_op_test('//=', [-1, 1.5, 3], [-5, 1.25, 2.5])
+        self.inplace_float_op_test(operator.ifloordiv, [-1, 1.5, 3], [-5, 1.25, 2.5])
 
     def test_inplace_div(self):
         self.inplace_float_op_test('/=', [-1, 1.5, 3], [-5, 0, 2.5])
+        self.inplace_float_op_test(operator.itruediv, [-1, 1.5, 3], [-5, 1.25, 2.5])
 
     def test_inplace_remainder(self):
         self.inplace_float_op_test('%=', [-1, 1.5, 3], [-5, 2, 2.5])
+        self.inplace_float_op_test(operator.imod, [-1, 1.5, 3], [-5, 2, 2.5])
 
     @tag('important')
     def test_inplace_pow(self):
         self.inplace_float_op_test('**=', [-1, 1.5, 3], [-5, 2, 2.5])
+        self.inplace_float_op_test(operator.ipow, [-1, 1.5, 3], [-5, 2, 2.5])
 
     def test_inplace_and(self):
         self.inplace_bitwise_op_test('&=', [0, 1, 2, 3, 51], [0, 13, 16, 42, 255])
+        self.inplace_bitwise_op_test(operator.iand, [0, 1, 2, 3, 51], [0, 13, 16, 42, 255])
 
     def test_inplace_or(self):
         self.inplace_bitwise_op_test('|=', [0, 1, 2, 3, 51], [0, 13, 16, 42, 255])
+        self.inplace_bitwise_op_test(operator.ior, [0, 1, 2, 3, 51], [0, 13, 16, 42, 255])
 
     def test_inplace_xor(self):
         self.inplace_bitwise_op_test('^=', [0, 1, 2, 3, 51], [0, 13, 16, 42, 255])
+        self.inplace_bitwise_op_test(operator.ixor, [0, 1, 2, 3, 51], [0, 13, 16, 42, 255])
 
     @tag('important')
     def test_inplace_lshift(self):
         self.inplace_int_op_test('<<=', [0, 5, -10, -51], [0, 1, 4, 14])
+        self.inplace_int_op_test(operator.ilshift, [0, 5, -10, -51], [0, 1, 4, 14])
 
     def test_inplace_rshift(self):
         self.inplace_int_op_test('>>=', [0, 5, -10, -51], [0, 1, 4, 14])
+        self.inplace_int_op_test(operator.irshift, [0, 5, -10, -51], [0, 1, 4, 14])
 
     def test_unary_positive_array_op(self):
         '''