Repository: scikit-hep/awkward

Issue:
Reducers raise IndexError on masked three dimensional array
### Version of Awkward Array

2.2.1

### Description and code to reproduce

```python
import awkward as ak
mask = ak.Array([False, False])
array = ak.mask(ak.unflatten(ak.mask(ak.unflatten([5, 6], [1,1]), [True, True]), [1,1]), mask)

ak.any(array, axis=2)
```
The last line raises `IndexError: index 0 is out of bounds for axis 0 with size 0`. However, I would except it to return `<Array [None, None] type='2 * option[var * ?bool]'>`
If `mask` has any elements that are true, it works.
I also tested this with `ak.all` and `ak.sum` with the same results.


Discussion:

Resolving patch:
diff --git a/src/awkward/contents/bytemaskedarray.py b/src/awkward/contents/bytemaskedarray.py
--- a/src/awkward/contents/bytemaskedarray.py
+++ b/src/awkward/contents/bytemaskedarray.py
@@ -949,10 +949,12 @@ def _reduce_next(
         if not branch and negaxis == depth:
             return out
         else:
-            if out.is_list:
-                out_content = out.content[out.starts[0] :]
-            elif out.is_regular:
+            if isinstance(out, ak.contents.RegularArray):
                 out_content = out.content
+            elif isinstance(out, ak.contents.ListOffsetArray):
+                # The `outindex` that will index into `out_content` is 0-based, so we should ensure that we normalise
+                # the list content to start at the first offset.
+                out_content = out.content[out.offsets[0] :]
             else:
                 raise ValueError(
                     "reduce_next with unbranching depth > negaxis is only "
diff --git a/src/awkward/contents/indexedoptionarray.py b/src/awkward/contents/indexedoptionarray.py
--- a/src/awkward/contents/indexedoptionarray.py
+++ b/src/awkward/contents/indexedoptionarray.py
@@ -1387,10 +1387,12 @@ def _reduce_next(
         if not branch and negaxis == depth:
             return out
         else:
-            if out.is_list:
-                out_content = out.content[out.starts[0] :]
-            elif out.is_regular:
+            if isinstance(out, ak.contents.RegularArray):
                 out_content = out.content
+            elif isinstance(out, ak.contents.ListOffsetArray):
+                # The `outindex` that will index into `out_content` is 0-based, so we should ensure that we normalise
+                # the list content to start at the first offset.
+                out_content = out.content[out.offsets[0] :]
             else:
                 raise AssertionError(
                     "reduce_next with unbranching depth > negaxis is only "


Test patch:
diff --git a/tests/test_2484_reduce_starts_empty.py b/tests/test_2484_reduce_starts_empty.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2484_reduce_starts_empty.py
@@ -0,0 +1,36 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+
+import numpy as np
+
+import awkward as ak
+
+
+def test():
+    layout = ak.contents.ByteMaskedArray(
+        ak.index.Index8([0]),
+        ak.contents.ListOffsetArray(
+            ak.index.Index64([0, 1]),
+            ak.contents.ByteMaskedArray(
+                ak.index.Index8([1]),
+                ak.contents.ListOffsetArray(
+                    ak.index.Index64([0, 2]), ak.contents.NumpyArray([5, 6])
+                ),
+                valid_when=True,
+            ),
+        ),
+        valid_when=True,
+    )
+    assert ak.almost_equal(
+        ak.any(layout, axis=2),
+        ak.contents.IndexedOptionArray(
+            ak.index.Index64([-1]),
+            ak.contents.ListOffsetArray(
+                ak.index.Index64([0]),
+                ak.contents.IndexedOptionArray(
+                    ak.index.Index64([]),
+                    ak.contents.NumpyArray(np.empty(0, dtype=np.bool_)),
+                ),
+            ),
+        ),
+    )