Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/upcoming_changes/10671.bug_fix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Fix shape mismatch ignored by in-place array operators
------------------------------------------------------

An in-place array operator (for example ``a &= b``) whose operands did not
broadcast was lowered without a shape check, so it silently produced a wrong
result instead of raising. Numba now validates the inputs against the output
shape on the caller-provided-output path and raises ``ValueError``, matching
NumPy.
38 changes: 38 additions & 0 deletions numba/np/npyimpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,34 @@ def store_data(self, indices, value):
assert ctx.get_data_type(self.base_type) == store_value.type
bld.store(store_value, self._load_effective_address(indices))

def guard_broadcast(self, loopshape, argno):
# Check that this input array broadcasts onto the output (loop)
# shape. Used when the output array is provided by the caller (an
# in-place operator like a += b or an explicit out=), where the
# broadcast check in _build_array is skipped (issue #9166).
msg = "unable to broadcast argument %d to output array" % argno
def raise_impl(loop_shape, array_shape):
nloop = len(loop_shape)
narr = len(array_shape)
incompatible = narr > nloop
if not incompatible:
offset = nloop - narr
for i in range(narr):
dim = array_shape[i]
if dim != 1 and dim != loop_shape[offset + i]:
incompatible = True
if incompatible:
raise ValueError(msg)

context, builder = self.context, self.builder
sig = types.none(
types.UniTuple(types.intp, len(loopshape)),
types.UniTuple(types.intp, len(self.shape)),
)
tup = (context.make_tuple(builder, sig.args[0], loopshape),
context.make_tuple(builder, sig.args[1], self.shape))
context.compile_internal(builder, raise_impl, sig, tup)


class _ArrayGUHelper(namedtuple('_ArrayHelper', ('context', 'builder',
'shape', 'strides', 'data',
Expand Down Expand Up @@ -498,6 +526,16 @@ def numpy_ufunc_kernel(context, builder, sig, args, ufunc, kernel_class):
"Not enough inputs to {}, expected {} got {}"
.format(ufunc.__name__, ufunc.nin, len(arguments)))

# When the output array is provided by the caller (an in-place operator
# like a += b or an explicit out=), the broadcast check that _build_array
# runs for an implicit output is skipped. Validate here that every input
# broadcasts onto the provided output shape (issue #9166).
for provided_output in arguments[ufunc.nin:]:
if isinstance(provided_output, _ArrayHelper):
for argno, inp in enumerate(arguments[:ufunc.nin]):
if isinstance(inp, _ArrayHelper):
inp.guard_broadcast(provided_output.shape, argno)

for out_i, ret_ty in enumerate(_unpack_output_types(ufunc, sig)):
if ufunc.nin + out_i >= len(arguments):
# this out argument is not provided
Expand Down
65 changes: 65 additions & 0 deletions numba/tests/test_ufuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,71 @@ def test_inplace_rshift(self):
self.inplace_int_op_test(operator.irshift, [0, 5, -10, -51],
[0, 1, 4, 14])

def test_inplace_broadcast_mismatch(self):
# Issue #9166: an in-place operator whose operands do not broadcast
# must raise like NumPy, not silently ignore the shape mismatch.
# Exceptions leak references
self.disable_leak_check()

@njit
def inplace_and(a, b):
a &= b
return a

@njit
def inplace_add(a, b):
a += b
return a

with self.assertRaises(ValueError) as raises:
inplace_and(np.array([True, False, True]),
np.array([True, False]))
self.assertIn("unable to broadcast", str(raises.exception))

with self.assertRaises(ValueError) as raises:
inplace_add(np.ones((3, 4)), np.ones(3))
self.assertIn("unable to broadcast", str(raises.exception))

# The input must not have more dimensions than the output either.
with self.assertRaises(ValueError):
inplace_add(np.ones(4), np.ones((3, 4)))

# Valid broadcasts keep working: right-aligned and size-1.
got = inplace_add(np.ones((3, 4)), np.arange(4.0))
np.testing.assert_array_equal(got, np.ones((3, 4)) + np.arange(4.0))
got = inplace_add(np.ones((3, 4)), np.ones(1))
np.testing.assert_array_equal(got, np.full((3, 4), 2.0))

def test_out_broadcast_errors(self):
# Issue #9166, ported from NumPy's
# TestUfunc.test_out_broadcast_errors. A caller-provided output may
# broadcast the inputs, but it cannot be smaller than the result, so
# a too-small out must raise instead of silently writing.
# Exceptions leak references
self.disable_leak_check()

@njit
def positive_out(a, out):
return np.positive(a, out)

@njit
def add_out(a, b, out):
return np.add(a, b, out)

cases = [
(np.ones(1), np.empty(())),
(np.ones(2), np.empty(1)),
(np.ones((4, 3)), np.empty((4, 1))),
]
for arr, out in cases:
with self.assertRaises(ValueError) as raises:
positive_out(arr, out)
self.assertIn("unable to broadcast", str(raises.exception))

with self.assertRaises(ValueError) as raises:
add_out(np.ones(()), arr, out)
self.assertIn("unable to broadcast", str(raises.exception))

def test_unary_positive_array_op_2(self):
'''
Verify that the unary positive operator copies values, and doesn't
Expand Down
Loading