Repository: scikit-hep/awkward

Issue:
RuntimeError when slicing two dimensions using a masked array
### Version of Awkward Array

1.8.0rc3

### Description and code to reproduce

Please consider the following

```python
import awkward as ak


x = ak.Array([[5.]])
y = ak.Array([True])

x[y, 0]
x[y.mask[[True]], 0]
```

The last line will raise the following error
`RuntimeError: ListOffsetArray::getitem_next(SliceAt): !advanced.is_empty_advanced()`
I don't think the line should raise an error and I am unsure what the error message is telling me.


Discussion:
That's right. This has reached some unconsidered code path. It is faithfully reproduced in v2:

```python
import awkward as ak
x = ak._v2.Array([[5.]])
y = ak._v2.Array([True])
x[ak._v2.mask(y, [True]), 0]
```
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jpivarski/irishep/awkward-1.0/awkward/_v2/highlevel.py", line 1010, in __getitem__
    out = self._layout[where]
  File "/home/jpivarski/irishep/awkward-1.0/awkward/_v2/contents/content.py", line 486, in __getitem__
    out = next._getitem_next(nextwhere[0], nextwhere[1:], None)
  File "/home/jpivarski/irishep/awkward-1.0/awkward/_v2/contents/regulararray.py", line 590, in _getitem_next
    return self._getitem_next_missing(head, tail, advanced)
  File "/home/jpivarski/irishep/awkward-1.0/awkward/_v2/contents/content.py", line 406, in _getitem_next_missing
    nextcontent = self._getitem_next(headcontent, tail, advanced)
  File "/home/jpivarski/irishep/awkward-1.0/awkward/_v2/contents/regulararray.py", line 492, in _getitem_next
    out = nextcontent._getitem_next(nexthead, nexttail, nextadvanced)
  File "/home/jpivarski/irishep/awkward-1.0/awkward/_v2/contents/listarray.py", line 511, in _getitem_next
    assert advanced is None
AssertionError
```

and it would probably be easier to debug in v2, as that's Python code.

The "`advanced`" Index is for passing down information needed in advanced indexing, which is to say, slicing more than one dimension by an array. That happens here because the integer `0` gets promoted to an array because it's in a tuple (NumPy rules: arrays and integers in a slice tuple get broadcasted together, which promotes integers into arrays).

When descending through a layout tree, we might have an `advanced` Index at some level or we might not. (When we first hit an array or index that has been broadcasted to one, we create the `advanced` Index and pass it to the whole subtree.) Early on, this was represented in v1 as an empty Index, but there were valid cases in which an `advanced` Index can exist and have zero length. So in v1, the `is_empty_advanced` boolean keeps track of whether this Index represents an existing Index or not.

In v2, we use `None` to say it isn't there. (Yay, Python!)

The slice of `x` has two dimensions, one of which is an Awkward Array with nullable data (`y.mask[[True]]`) and the other is an integer, promoted to an array. Whereas NumPy expresses slices on multiple dimensions as multiple arrays in a tuple,

```python
array[slice_dimension_1, slice_dimension_2]
```

Awkward Array supports this as much as it has to and extends it in a different way: by requiring the nested structure of the slicer and the sliced to match (broadcastable):

```python
jagged_jagged_array[another_jagged_jagged_array]
```

What happens when you mix the two is minimally tested:

```python
jagged_jagged_array[a_jagged_array, deeper_index]
```

A semantics has been defined: the `deeper_index` continues the recursion in the layout tree from the point where `a_jagged_array` stops. The case you described may or may not be allowed (I'd have to think hard about that), but this is unquestionably a bug in the sense that if you weren't supposed to do that, you'd get a ValueError, IndexError, or TypeError with an informative message, not a RuntimeError (v1's C++) or AssertionError (v2), indicating a programming error.

Resolving patch:
diff --git a/src/awkward/_v2/_slicing.py b/src/awkward/_v2/_slicing.py
--- a/src/awkward/_v2/_slicing.py
+++ b/src/awkward/_v2/_slicing.py
@@ -18,13 +18,16 @@ def headtail(oldtail):
 def getitem_broadcast(items):
     lookup = []
     broadcastable = []
+    awkward_items = 0
     for item in items:
+        if isinstance(item, ak._v2.contents.Content):
+            awkward_items += 1
         if (
             isinstance(
                 item,
                 (
                     slice,
-                    list,
+                    list,  # of strings
                     ak._v2.contents.ListOffsetArray,
                     ak._v2.contents.IndexedOptionArray,
                 ),
@@ -35,9 +38,17 @@ def getitem_broadcast(items):
         ):
             lookup.append(None)
         else:
+            # this includes integers (which broadcast to arrays)
             lookup.append(len(broadcastable))
             broadcastable.append(item)
 
+    if awkward_items > 1 or (awkward_items == 1 and len(broadcastable) != 0):
+        raise ak._v2._util.error(
+            TypeError(
+                "cannot mix Awkward slicing (using an array with missing or variable-length lists in the slice) with NumPy advanced slicing (using more than one broadcastable array or integer in the slice), though you can perform multiple slices"
+            )
+        )
+
     nplike = ak.nplike.of(*broadcastable)
 
     broadcasted = nplike.broadcast_arrays(*broadcastable)
@@ -354,7 +365,7 @@ def prepare_tuple_bool_to_int(item):
             if item.content.content.data.shape[0] > 0:
                 expanded = item.content.content.data[safeindex]
             else:
-                expanded = item.content.content.data.nplike.ones(
+                expanded = item.content.content.nplike.ones(
                     safeindex.shape[0], np.bool_
                 )
 
@@ -423,9 +434,7 @@ def prepare_tuple_bool_to_int(item):
                 if item.content.data.shape[0] > 0:
                     expanded = item.content.data[safeindex]
                 else:
-                    expanded = item.content.data.nplike.ones(
-                        safeindex.shape[0], np.bool_
-                    )
+                    expanded = item.content.nplike.ones(safeindex.shape[0], np.bool_)
 
                 # nextcontent does not include missing values
                 expanded[isnegative] = False
@@ -448,7 +457,7 @@ def prepare_tuple_bool_to_int(item):
                 )
 
             return ak._v2.contents.IndexedOptionArray(
-                ak._v2.index.Index(outindex, item.nplike),
+                ak._v2.index.Index(outindex, nplike=item.nplike),
                 ak._v2.contents.NumpyArray(nextcontent, nplike=item.nplike),
             )
 
diff --git a/src/awkward/_v2/contents/unionarray.py b/src/awkward/_v2/contents/unionarray.py
--- a/src/awkward/_v2/contents/unionarray.py
+++ b/src/awkward/_v2/contents/unionarray.py
@@ -291,7 +291,9 @@ def project(self, index):
                 index,
             )
         )
-        nextcarry = ak._v2.index.Index64(tmpcarry.data[: lenout[0]], self._nplike)
+        nextcarry = ak._v2.index.Index64(
+            tmpcarry.data[: lenout[0]], nplike=self._nplike
+        )
         return self._contents[index]._carry(nextcarry, False)
 
     @staticmethod
diff --git a/src/awkward/_v2/index.py b/src/awkward/_v2/index.py
--- a/src/awkward/_v2/index.py
+++ b/src/awkward/_v2/index.py
@@ -20,6 +20,8 @@ def __init__(self, data, metadata=None, nplike=None):
         if nplike is None:
             nplike = ak.nplike.of(data)
         self._nplike = nplike
+        if metadata is not None and not isinstance(metadata, dict):
+            raise ak._v2._util.error(TypeError("Index metadata must be None or a dict"))
         self._metadata = metadata
         self._data = self._nplike.index_nplike.asarray(
             data, dtype=self._expected_dtype, order="C"


Test patch:
diff --git a/tests/v2/test_1305-mixed-awkward-numpy-slicing.py b/tests/v2/test_1305-mixed-awkward-numpy-slicing.py
new file mode 100644
--- /dev/null
+++ b/tests/v2/test_1305-mixed-awkward-numpy-slicing.py
@@ -0,0 +1,13 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import pytest  # noqa: F401
+import numpy as np  # noqa: F401
+import awkward as ak  # noqa: F401
+
+
+def test():
+    array = ak._v2.Array([[3.14]])
+    first_slice = ak._v2.Array([True, None])[:1]  # type is 1 * ?bool
+    second_slice = 0
+    with pytest.raises(TypeError):
+        array[first_slice, second_slice]