Repository: scikit-hep/awkward

Issue:
ak.singletons fails on certain typetracers
### Version of Awkward Array

2.1.3

### Description and code to reproduce

The following script:
```python3
import json
import awkward as ak
import numpy as np

fromjson = {
    "class": "RecordArray",
    "fields": ["muon"],
    "contents": [
        {
            "class": "ListOffsetArray",
            "offsets": "i64",
            "content": {
                "class": "RecordArray",
                "fields": ["pt", "eta", "phi", "crossref", "thing1"],
                "contents": [
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "muon_pt!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "muon_eta!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "muon_phi!",
                    },
                    {
                        "class": "ListOffsetArray",
                        "offsets": "i64",
                        "content": {
                            "class": "NumpyArray",
                            "primitive": "int64",
                            "inner_shape": [],
                            "parameters": {},
                            "form_key": "muon_crossref_content!",
                        },
                        "parameters": {},
                        "form_key": "muon_crossref_index!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "muon_thing1!",
                    },
                ],
                "parameters": {},
                "form_key": "muon_record!",
            },
            "parameters": {},
            "form_key": "muon_list_outer!",
        },
    ],
    "parameters": {},
    "form_key": "outer!",
}

form = ak.forms.from_json(json.dumps(fromjson))

ttlayout, report = ak._nplikes.typetracer.typetracer_with_report(form, forget_length=True)

ttarray = ak.Array(ttlayout, behavior=ak.behavior)

sgltns_idx = ak.singletons(ak.argmin(ttarray.muon.pt, axis=1))
```

results in:
```
Traceback (most recent call last):
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/operations/ak_singletons.py", line 47, in singletons
    return _impl(array, axis, highlevel, behavior)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/operations/ak_singletons.py", line 85, in _impl
    out = ak._do.recursively_apply(layout, action, behavior, numpy_to_regular=True)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/_do.py", line 34, in recursively_apply
    return layout._recursively_apply(
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/contents/bytemaskedarray.py", line 1082, in _recursively_apply
    result = action(
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/operations/ak_singletons.py", line 76, in action
    ak.index.Index64(offsets), layout.project()
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/contents/bytemaskedarray.py", line 626, in project
    mask_length - numnull[0], nplike=self._backend.index_nplike
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/numpy/lib/mixins.py", line 31, in func
    return ufunc(other, self)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/_nplikes/typetracer.py", line 532, in __array_ufunc__
    return self.nplike._apply_ufunc(ufunc, *inputs)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/_nplikes/typetracer.py", line 570, in _apply_ufunc
    broadcasted = self.broadcast_arrays(*inputs)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/_nplikes/typetracer.py", line 1003, in broadcast_arrays
    x = self.promote_scalar(x)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/_nplikes/typetracer.py", line 843, in promote_scalar
    raise TypeError(f"expected scalar type, received {obj}")
TypeError: expected scalar type, received ##

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "form_madness7.py", line 77, in <module>
    sgltns_idx = ak.singletons(ak.argmin(ttarray.muon.pt, axis=1))
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/operations/ak_singletons.py", line 47, in singletons
    return _impl(array, axis, highlevel, behavior)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/_errors.py", line 54, in __exit__
    self.handle_exception(exception_type, exception_value)
  File "/Users/lgray/miniforge3/envs/coffea-dev/lib/python3.8/site-packages/awkward/_errors.py", line 64, in handle_exception
    raise self.decorate_exception(cls, exception)
TypeError: expected scalar type, received ##

This error occurred while calling

    ak.singletons(
        array = <Array-typetracer [...] type='## * ?int64'>
        axis = 0
        highlevel = True
        behavior = None
    )
```


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
@@ -568,7 +568,7 @@ def _getitem_next(
 
     def project(self, mask=None):
         mask_length = self._mask.length
-        numnull = ak.index.Index64.zeros(1, nplike=self._backend.index_nplike)
+        _numnull = ak.index.Index64.zeros(1, nplike=self._backend.index_nplike)
 
         if mask is not None:
             if self._backend.nplike.known_data and mask_length != mask.length:
@@ -608,23 +608,24 @@ def project(self, mask=None):
 
         else:
             assert (
-                numnull.nplike is self._backend.index_nplike
+                _numnull.nplike is self._backend.index_nplike
                 and self._mask.nplike is self._backend.index_nplike
             )
             self._handle_error(
                 self._backend[
                     "awkward_ByteMaskedArray_numnull",
-                    numnull.dtype.type,
+                    _numnull.dtype.type,
                     self._mask.dtype.type,
                 ](
-                    numnull.data,
+                    _numnull.data,
                     self._mask.data,
                     mask_length,
                     self._valid_when,
                 )
             )
+            numnull = self._backend.index_nplike.index_as_shape_item(_numnull[0])
             nextcarry = ak.index.Index64.empty(
-                mask_length - numnull[0], nplike=self._backend.index_nplike
+                mask_length - numnull, nplike=self._backend.index_nplike
             )
             assert (
                 nextcarry.nplike is self._backend.index_nplike


Test patch:
diff --git a/tests/test_2417_bytemasked_singletons.py b/tests/test_2417_bytemasked_singletons.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2417_bytemasked_singletons.py
@@ -0,0 +1,26 @@
+# 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():
+    array = ak.Array(
+        ak.contents.ByteMaskedArray(
+            mask=ak.index.Index8([1, 1, 0, 0]),
+            content=ak.contents.NumpyArray([3, 4, 2, 5]),
+            valid_when=True,
+        )
+    )
+    assert (
+        ak.singletons(array, highlevel=False).form
+        == ak.singletons(ak.to_backend(array, "typetracer"), highlevel=False).form
+    )
+
+    assert (
+        ak.singletons(array, highlevel=False).form
+        == ak.singletons(
+            array.layout.to_typetracer(forget_length=True), highlevel=False
+        ).form
+    )