Repository: scikit-hep/awkward

Issue:
Broadcasting a zero-length dimensions differs from NumPy
### Version of Awkward Array

main

### Description and code to reproduce

`np.broadcast_shapes((4, 0, 3), (1, 1, 3))` produces the shape `(4, 0, 3)`. The equivalent operation with `ak.broadcast_arrays` does not succeed.

We don't have to make this work for broadcasting ragged arrays against regular arrays, in my view.


Discussion:

Resolving patch:
diff --git a/src/awkward/_broadcasting.py b/src/awkward/_broadcasting.py
--- a/src/awkward/_broadcasting.py
+++ b/src/awkward/_broadcasting.py
@@ -670,18 +670,32 @@ def continuation():
                 isinstance(x, RegularArray) or not isinstance(x, listtypes)
                 for x in inputs
             ):
-                maxsize = max(x.size for x in inputs if isinstance(x, RegularArray))
+                # Ensure all layouts have same length
+                length = None
+                for x in inputs:
+                    if isinstance(x, Content):
+                        if length is None:
+                            length = x.length
+                        elif backend.nplike.known_shape:
+                            assert length == x.length
+                assert length is not None
+
+                if any(x.size == 0 for x in inputs if isinstance(x, RegularArray)):
+                    dimsize = 0
+                else:
+                    dimsize = max(x.size for x in inputs if isinstance(x, RegularArray))
 
                 if backend.nplike.known_data:
                     for x in inputs:
                         if isinstance(x, RegularArray):
-                            if maxsize > 1 and x.size == 1:
+                            if dimsize > 1 and x.size == 1:
+                                # For any (N, 1) array, we know we'll broadcast to (N, M) where M is maxsize
                                 tmpindex = Index64(
                                     backend.index_nplike.repeat(
                                         backend.index_nplike.arange(
                                             x.length, dtype=np.int64
                                         ),
-                                        maxsize,
+                                        dimsize,
                                     ),
                                     nplike=backend.index_nplike,
                                 )
@@ -689,20 +703,22 @@ def continuation():
                     nextinputs = []
                     for x in inputs:
                         if isinstance(x, RegularArray):
-                            if maxsize > 1 and x.size == 1:
+                            if dimsize > 1 and x.size == 1:
                                 nextinputs.append(
                                     x.content[: x.length * x.size]._carry(
                                         tmpindex, allow_lazy=False
                                     )
                                 )
-                            elif x.size == maxsize:
+                            elif x.size == dimsize:
                                 nextinputs.append(x.content[: x.length * x.size])
+                            elif dimsize == 0:
+                                nextinputs.append(x.content[:0])
                             else:
                                 raise ak._errors.wrap_error(
                                     ValueError(
                                         "cannot broadcast RegularArray of size "
                                         "{} with RegularArray of size {} {}".format(
-                                            x.size, maxsize, in_function(options)
+                                            x.size, dimsize, in_function(options)
                                         )
                                     )
                                 )
@@ -718,15 +734,6 @@ def continuation():
                         else:
                             nextinputs.append(x)
 
-                length = None
-                for x in inputs:
-                    if isinstance(x, Content):
-                        if length is None:
-                            length = x.length
-                        elif backend.nplike.known_shape:
-                            assert length == x.length
-                assert length is not None
-
                 outcontent = apply_step(
                     backend,
                     nextinputs,
@@ -740,7 +747,7 @@ def continuation():
                 assert isinstance(outcontent, tuple)
                 parameters = parameters_factory(len(outcontent))
                 return tuple(
-                    RegularArray(x, maxsize, length, parameters=p)
+                    RegularArray(x, dimsize, length, parameters=p)
                     for x, p in zip(outcontent, parameters)
                 )
 
diff --git a/src/awkward/_util.py b/src/awkward/_util.py
--- a/src/awkward/_util.py
+++ b/src/awkward/_util.py
@@ -931,7 +931,10 @@ def visitor(left, right) -> bool:
                 and (left.is_tuple == right.is_tuple)
                 and all([visitor(x, y) for x, y in zip(left.contents, right.contents)])
             )
-        elif left.is_empty:
+        elif left.is_unknown:
             return True
 
+        else:
+            raise ak._errors.wrap_error(AssertionError)
+
     return visitor(left, right)


Test patch:
diff --git a/tests/test_2082_broadcast_zero_size.py b/tests/test_2082_broadcast_zero_size.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2082_broadcast_zero_size.py
@@ -0,0 +1,19 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import numpy as np
+import pytest  # noqa: F401
+
+import awkward as ak
+
+
+@pytest.mark.parametrize("axis", [0, 1, 2])
+def test_axis(axis):
+    this_slice = (slice(None),) * axis + (slice(0, 0),) + (...,)
+    that_slice = (slice(None),) * axis + (slice(0, 1),) + (...,)
+    x = np.arange(3 * 5 * 7).reshape(3, 5, 7)
+    result = np.broadcast_arrays(x[this_slice], x[that_slice])
+
+    y = ak.from_numpy(x)
+    result_ak = ak.broadcast_arrays(y[this_slice], y[that_slice])
+
+    assert ak._util.arrays_approx_equal(result, result_ak)