Repository: scikit-hep/awkward

Issue:
`ak.full_like` does not fill with correct value when `dtype` is changed
### Version of Awkward Array

2.0.8

### Description and code to reproduce

For example:
```python
>>> import awkward as ak
>>> ak.full_like([True], 2, dtype=int)
<Array [1] type='1 * int64'>
```

I would expect the array to get filled with `dtype(fill_value)`, so obviously just `2` in this case. This is the behavior in NumPy:

```python
>>> import numpy as np
>>> np.full_like([True], 2, dtype=int)
array([2])
```


Discussion:
At first I thought maybe it was just for `bool` arrays, but it seems that the `fill_value` is converted to the original array's type, then to the `dtype` actually specified. So the problem occurs when there's information lost in converting `fill_type` to the original array's type. So going from a `float` array to `int`s is usually fine:

```python
>>> ak.full_like(ak.Array([1.0]), 2, dtype=int)
<Array [2] type='1 * int64'>
```

but not the other way around:

```python
>>> ak.full_like(ak.Array([1]), 2.3, dtype=float)
<Array [2] type='1 * float64'>

```

Resolving patch:
diff --git a/src/awkward/operations/ak_full_like.py b/src/awkward/operations/ak_full_like.py
--- a/src/awkward/operations/ak_full_like.py
+++ b/src/awkward/operations/ak_full_like.py
@@ -10,12 +10,23 @@
 np = NumpyMetadata.instance()
 
 
-def full_like(array, fill_value, *, dtype=None, highlevel=True, behavior=None):
+def full_like(
+    array,
+    fill_value,
+    *,
+    dtype=None,
+    including_unknown=False,
+    highlevel=True,
+    behavior=None,
+):
     """
     Args:
         array: Array-like data (anything #ak.to_layout recognizes).
         fill_value: Value to fill the new array with.
         dtype (None or NumPy dtype): Overrides the data type of the result.
+        including_unknown (bool): If True, the `unknown` type is considered
+            a value type and is converted to a zero-length array of the
+            specified dtype; if False, `unknown` will remain `unknown`.
         highlevel (bool, default is True): If True, return an #ak.Array;
             otherwise, return a low-level #ak.contents.Content subclass.
         behavior (None or dict): Custom #ak.behavior for the output array, if
@@ -72,14 +83,15 @@ def full_like(array, fill_value, *, dtype=None, highlevel=True, behavior=None):
             "array": array,
             "fill_value": fill_value,
             "dtype": dtype,
+            "including_unknown": including_unknown,
             "highlevel": highlevel,
             "behavior": behavior,
         },
     ):
-        return _impl(array, fill_value, highlevel, behavior, dtype)
+        return _impl(array, fill_value, highlevel, behavior, dtype, including_unknown)
 
 
-def _impl(array, fill_value, highlevel, behavior, dtype):
+def _impl(array, fill_value, highlevel, behavior, dtype, including_unknown):
     if dtype is not None:
         # In the case of strings and byte strings,
         # converting the fill avoids a ValueError.
@@ -99,99 +111,104 @@ def action(layout, **kwargs):
         nplike = layout.backend.nplike
         index_nplike = layout.backend.index_nplike
 
-        if layout.parameter("__array__") == "bytestring" and fill_value is _ZEROS:
-            asbytes = nplike.frombuffer(b"", dtype=np.uint8)
-            return ak.contents.ListArray(
-                ak.index.Index64(
-                    index_nplike.zeros(layout.length, dtype=np.int64),
-                    nplike=index_nplike,
-                ),
-                ak.index.Index64(
-                    index_nplike.zeros(layout.length, dtype=np.int64),
-                    nplike=index_nplike,
-                ),
-                ak.contents.NumpyArray(asbytes, parameters={"__array__": "byte"}),
-                parameters={"__array__": "bytestring"},
-            )
-
-        elif layout.parameter("__array__") == "bytestring":
-            if isinstance(fill_value, bytes):
-                asbytes = fill_value
-            else:
-                asbytes = str(fill_value).encode("utf-8", "surrogateescape")
-            asbytes = nplike.frombuffer(asbytes, dtype=np.uint8)
-
-            return ak.contents.ListArray(
-                ak.index.Index64(
-                    index_nplike.zeros(layout.length, dtype=np.int64),
-                    nplike=index_nplike,
-                ),
-                ak.index.Index64(
-                    index_nplike.full(layout.length, len(asbytes), dtype=np.int64)
-                ),
-                ak.contents.NumpyArray(asbytes, parameters={"__array__": "byte"}),
-                parameters={"__array__": "bytestring"},
-            )
-
-        elif layout.parameter("__array__") == "string" and fill_value is _ZEROS:
-            asbytes = nplike.frombuffer(b"", dtype=np.uint8)
-            return ak.contents.ListArray(
-                ak.index.Index64(
-                    index_nplike.zeros(layout.length, dtype=np.int64),
-                    nplike=index_nplike,
-                ),
-                ak.index.Index64(
-                    index_nplike.zeros(layout.length, dtype=np.int64),
-                    nplike=index_nplike,
-                ),
-                ak.contents.NumpyArray(asbytes, parameters={"__array__": "char"}),
-                parameters={"__array__": "string"},
-            )
-
-        elif layout.parameter("__array__") == "string":
-            asstr = str(fill_value).encode("utf-8", "surrogateescape")
-            asbytes = nplike.frombuffer(asstr, dtype=np.uint8)
-            return ak.contents.ListArray(
-                ak.index.Index64(
-                    index_nplike.zeros(layout.length, dtype=np.int64),
-                    nplike=index_nplike,
-                ),
-                ak.index.Index64(
-                    index_nplike.full(layout.length, len(asbytes), dtype=np.int64)
-                ),
-                ak.contents.NumpyArray(asbytes, parameters={"__array__": "char"}),
-                parameters={"__array__": "string"},
-            )
-
-        elif isinstance(layout, ak.contents.NumpyArray):
+        if layout.is_numpy:
             original = nplike.asarray(layout.data)
 
-            if fill_value is _ZEROS or (ensure_known_scalar(fill_value == 0, False)):
+            if fill_value is _ZEROS or ensure_known_scalar(fill_value == 0, False):
                 return ak.contents.NumpyArray(
-                    nplike.zeros_like(original), parameters=layout.parameters
+                    nplike.zeros_like(original, dtype=dtype),
+                    parameters=layout.parameters,
                 )
             elif ensure_known_scalar(fill_value == 1, False):
                 return ak.contents.NumpyArray(
-                    nplike.ones_like(original), parameters=layout.parameters
+                    nplike.ones_like(original, dtype=dtype),
+                    parameters=layout.parameters,
                 )
             else:
                 return ak.contents.NumpyArray(
-                    nplike.full_like(original, fill_value), parameters=layout.parameters
+                    nplike.full_like(original, fill_value, dtype=dtype),
+                    parameters=layout.parameters,
+                )
+
+        elif layout.is_unknown:
+            if dtype is not None and including_unknown:
+                return layout.to_NumpyArray(dtype=dtype)
+            else:
+                return None
+
+        elif layout.parameter("__array__") in {"bytestring", "string"}:
+            stringlike_type = layout.parameter("__array__")
+            if fill_value is _ZEROS:
+                asbytes = nplike.frombuffer(b"", dtype=np.uint8)
+                result = ak.contents.ListArray(
+                    ak.index.Index64(
+                        index_nplike.zeros(layout.length, dtype=np.int64),
+                        nplike=index_nplike,
+                    ),
+                    ak.index.Index64(
+                        index_nplike.zeros(layout.length, dtype=np.int64),
+                        nplike=index_nplike,
+                    ),
+                    ak.contents.NumpyArray(
+                        asbytes,
+                        parameters={
+                            "__array__": "byte"
+                            if stringlike_type == "bytestring"
+                            else "char"
+                        },
+                    ),
+                    parameters={"__array__": stringlike_type},
+                )
+
+            elif stringlike_type == "bytestring":
+                if isinstance(fill_value, bytes):
+                    asbytes = fill_value
+                else:
+                    asbytes = str(fill_value).encode("utf-8", "surrogateescape")
+                asbytes = nplike.frombuffer(asbytes, dtype=np.uint8)
+
+                result = ak.contents.ListArray(
+                    ak.index.Index64(
+                        index_nplike.zeros(layout.length, dtype=np.int64),
+                        nplike=index_nplike,
+                    ),
+                    ak.index.Index64(
+                        index_nplike.full(layout.length, len(asbytes), dtype=np.int64)
+                    ),
+                    ak.contents.NumpyArray(asbytes, parameters={"__array__": "byte"}),
+                    parameters={"__array__": "bytestring"},
                 )
+
+            else:
+                assert stringlike_type == "string"
+                asstr = str(fill_value).encode("utf-8", "surrogateescape")
+                asbytes = nplike.frombuffer(asstr, dtype=np.uint8)
+                result = ak.contents.ListArray(
+                    ak.index.Index64(
+                        index_nplike.zeros(layout.length, dtype=np.int64),
+                        nplike=index_nplike,
+                    ),
+                    ak.index.Index64(
+                        index_nplike.full(layout.length, len(asbytes), dtype=np.int64)
+                    ),
+                    ak.contents.NumpyArray(asbytes, parameters={"__array__": "char"}),
+                    parameters={"__array__": "string"},
+                )
+            if dtype is not None:
+                # Interpret strings as numeric/bool types
+                result = ak.operations.strings_astype(
+                    result, dtype, highlevel=highlevel, behavior=behavior
+                )
+                # Convert dtype
+                result = ak.operations.values_astype(
+                    result, dtype, highlevel=False, behavior=behavior
+                )
+            return result
         else:
             return None
 
     out = ak._do.recursively_apply(layout, action, behavior)
-    if dtype is not None:
-        out = ak.operations.strings_astype(
-            out, dtype, highlevel=highlevel, behavior=behavior
-        )
-        out = ak.operations.values_astype(
-            out, dtype, highlevel=highlevel, behavior=behavior
-        )
-        return out
-    else:
-        return ak._util.wrap(out, behavior, highlevel)
+    return ak._util.wrap(out, behavior, highlevel)
 
 
 @ak._connect.numpy.implements("full_like")
diff --git a/src/awkward/operations/ak_ones_like.py b/src/awkward/operations/ak_ones_like.py
--- a/src/awkward/operations/ak_ones_like.py
+++ b/src/awkward/operations/ak_ones_like.py
@@ -7,11 +7,16 @@
 np = NumpyMetadata.instance()
 
 
-def ones_like(array, *, dtype=None, highlevel=True, behavior=None):
+def ones_like(
+    array, *, dtype=None, including_unknown=False, highlevel=True, behavior=None
+):
     """
     Args:
         array: Array-like data (anything #ak.to_layout recognizes).
         dtype (None or NumPy dtype): Overrides the data type of the result.
+        including_unknown (bool): If True, the `unknown` type is considered
+            a value type and is converted to a zero-length array of the
+            specified dtype; if False, `unknown` will remain `unknown`.
         highlevel (bool, default is True): If True, return an #ak.Array;
             otherwise, return a low-level #ak.contents.Content subclass.
         behavior (None or dict): Custom #ak.behavior for the output array, if
@@ -26,13 +31,21 @@ def ones_like(array, *, dtype=None, highlevel=True, behavior=None):
     """
     with ak._errors.OperationErrorContext(
         "ak.ones_like",
-        {"array": array, "dtype": dtype, "highlevel": highlevel, "behavior": behavior},
+        {
+            "array": array,
+            "dtype": dtype,
+            "including_unknown": including_unknown,
+            "highlevel": highlevel,
+            "behavior": behavior,
+        },
     ):
-        return _impl(array, highlevel, behavior, dtype)
+        return _impl(array, highlevel, behavior, dtype, including_unknown)
 
 
-def _impl(array, highlevel, behavior, dtype):
-    return ak.operations.ak_full_like._impl(array, 1, highlevel, behavior, dtype)
+def _impl(array, highlevel, behavior, dtype, including_unknown):
+    return ak.operations.ak_full_like._impl(
+        array, 1, highlevel, behavior, dtype, including_unknown
+    )
 
 
 @ak._connect.numpy.implements("ones_like")
diff --git a/src/awkward/operations/ak_zeros_like.py b/src/awkward/operations/ak_zeros_like.py
--- a/src/awkward/operations/ak_zeros_like.py
+++ b/src/awkward/operations/ak_zeros_like.py
@@ -10,11 +10,16 @@
 _ZEROS = object()
 
 
-def zeros_like(array, *, dtype=None, highlevel=True, behavior=None):
+def zeros_like(
+    array, *, dtype=None, including_unknown=False, highlevel=True, behavior=None
+):
     """
     Args:
         array: Array-like data (anything #ak.to_layout recognizes).
         dtype (None or NumPy dtype): Overrides the data type of the result.
+        including_unknown (bool): If True, the `unknown` type is considered
+            a value type and is converted to a zero-length array of the
+            specified dtype; if False, `unknown` will remain `unknown`.
         highlevel (bool, default is True): If True, return an #ak.Array;
             otherwise, return a low-level #ak.contents.Content subclass.
         behavior (None or dict): Custom #ak.behavior for the output array, if
@@ -29,15 +34,25 @@ def zeros_like(array, *, dtype=None, highlevel=True, behavior=None):
     """
     with ak._errors.OperationErrorContext(
         "ak.zeros_like",
-        {"array": array, "dtype": dtype, "highlevel": highlevel, "behavior": behavior},
+        {
+            "array": array,
+            "dtype": dtype,
+            "including_unknown": including_unknown,
+            "highlevel": highlevel,
+            "behavior": behavior,
+        },
     ):
-        return _impl(array, highlevel, behavior, dtype)
+        return _impl(array, highlevel, behavior, dtype, including_unknown)
 
 
-def _impl(array, highlevel, behavior, dtype):
+def _impl(array, highlevel, behavior, dtype, including_unknown):
     if dtype is not None:
-        return ak.operations.ak_full_like._impl(array, 0, highlevel, behavior, dtype)
-    return ak.operations.ak_full_like._impl(array, _ZEROS, highlevel, behavior, dtype)
+        return ak.operations.ak_full_like._impl(
+            array, 0, highlevel, behavior, dtype, including_unknown
+        )
+    return ak.operations.ak_full_like._impl(
+        array, _ZEROS, highlevel, behavior, dtype, including_unknown
+    )
 
 
 @ak._connect.numpy.implements("zeros_like")


Test patch:
diff --git a/tests/test_2250_full_like_bool.py b/tests/test_2250_full_like_bool.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2250_full_like_bool.py
@@ -0,0 +1,24 @@
+# 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
+
+
+def test_bool():
+    result = ak.full_like([True], 2, dtype=np.int64)
+    assert ak.almost_equal(result, np.asarray([2], dtype=np.int64))
+
+
+def test_empty():
+    result = ak.full_like([], 2, dtype=np.int64)
+    assert result.layout.is_unknown
+    result = ak.full_like([], 2, dtype=np.int64, including_unknown=True)
+    assert result.layout.is_numpy
+    assert result.layout.dtype == np.dtype(np.int64)
+
+
+def test_complex128():
+    result = ak.full_like([1, 2], 4j, dtype=np.complex128)
+    assert ak.almost_equal(result, np.asarray([0 + 4j, 0 + 4j], dtype=np.complex128))