Repository: scikit-hep/awkward

Issue:
`IndexError` for `ak.argsort` on arrays with `None` 
### Version of Awkward Array

2.4.6

### Description and code to reproduce

It seems that `ak.argsort` does not work on arrays that are entirely `None`, though `ak.sort` works fine. I would have naively expected that if `sort` works on an array, `argsort` would also work on the array, so it is not clear to me if this behavior would be expected or not. Here is an example that reproduces the behavior: 
```
import awkward as ak
  
a = ak.Array([None,None,[None,None]])
print(ak.sort(a,axis=1))
print(ak.argsort(a,axis=1))
```  
The `ak.sort` line is ok (and prints `[None, None, [None, None]]`), while the `ak.argsort` line fails with the error below: 
```
Traceback (most recent call last):
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/contents/numpyarray.py", line 301, in _getitem_at
    out = self._data[where]
          ~~~~~~~~~~^^^^^^^
IndexError: index 2 is out of bounds for axis 0 with size 2

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/k.mohrman/coffea_dir/ewkcoffea/analysis/wwz/mre2.py", line 6, in <module>
    print(ak.argsort(a,axis=1))
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/highlevel.py", line 1233, in __str__
    return awkward._prettyprint.valuestr(self, 1, 80)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/_prettyprint.py", line 240, in valuestr
    _, strs = valuestr_horiz(data, limit_cols)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/_prettyprint.py", line 135, in valuestr_horiz
    cols_taken, strs = valuestr_horiz(current, limit_cols - 2)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/_prettyprint.py", line 117, in valuestr_horiz
    current = get_at(data, index)
              ^^^^^^^^^^^^^^^^^^^
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/_prettyprint.py", line 43, in get_at
    out = data._layout._getitem_at(index)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/contents/indexedoptionarray.py", line 308, in _getitem_at
    return self._content._getitem_at(self._index[where])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/k.mohrman/miniconda3/envs/ak-env/lib/python3.12/site-packages/awkward/contents/numpyarray.py", line 303, in _getitem_at
    raise ak._errors.index_error(self, where, str(err)) from err
IndexError: cannot slice NumpyArray (of length 2) with 2: index 2 is out of bounds for axis 0 with size 2
```


Discussion:
From the error, it looks like `ak.argsort` is actually succeeding; it's the pretty-printing that's failing. Let me take a look at that!

Resolving patch:
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
@@ -1265,10 +1265,15 @@ def _argsort_next(
                     nextoutindex.length,
                 )
             )
-
-        out = ak.contents.IndexedOptionArray.simplified(
-            nextoutindex, out, parameters=self._parameters
-        )
+            # Drop the option type: we have ensured that we don't have any
+            # -1 values in `nextoutindex` now!
+            out = out._carry(nextoutindex, False).copy(
+                parameters=parameters_union(out._parameters, self._parameters)
+            )
+        else:
+            out = ak.contents.IndexedOptionArray.simplified(
+                nextoutindex, out, parameters=self._parameters
+            )
 
         inject_nones = (
             True


Test patch:
diff --git a/tests/test_2769_argsort_all_none.py b/tests/test_2769_argsort_all_none.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2769_argsort_all_none.py
@@ -0,0 +1,51 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import pytest  # noqa: F401
+
+import awkward as ak
+
+
+def test_all_none():
+    result = ak.argsort([None, None, None])
+    assert ak.is_valid(result)
+    assert result.to_list() == [0, 1, 2]
+    assert isinstance(result.layout, ak.contents.NumpyArray)
+    assert result.type == ak.types.ArrayType(ak.types.NumpyType("int64"), 3)
+
+
+def test_mixed_none_local():
+    result = ak.argsort([None, 1, None, 2, 0, None, -1])
+    assert ak.is_valid(result)
+    assert result.to_list() == [6, 4, 1, 3, 0, 2, 5]
+    assert result.type == ak.types.ArrayType(ak.types.NumpyType("int64"), 7)
+
+
+def test_mixed_none_2d_local():
+    result = ak.argsort(
+        [
+            [None, 1, None, 0, None, None, -1],
+            None,
+            [None, 2, None, 2, 0, None, -2],
+        ],
+        axis=1,
+    )
+    assert ak.is_valid(result)
+    assert result.to_list() == [[6, 3, 1, 0, 2, 4, 5], None, [6, 4, 1, 3, 0, 2, 5]]
+    assert result.type == ak.types.ArrayType(
+        ak.types.OptionType(ak.types.ListType(ak.types.NumpyType("int64"))), 3
+    )
+
+
+def test_mixed_none_2d_nonlocal():
+    result = ak.argsort(
+        [
+            [None, 1, None, 0, None, None, -1],
+            [None, 2, None, 2, 0, None, -2],
+        ],
+        axis=0,
+    )
+    assert ak.is_valid(result)
+    assert result.to_list() == [[0, 0, 0, 0, 1, 0, 1], [1, 1, 1, 1, 0, 1, 0]]
+    assert result.type == ak.types.ArrayType(
+        ak.types.ListType(ak.types.NumpyType("int64")), 2
+    )