Repository: scikit-hep/awkward

Issue:
`ak.fill_none` doesn't support option-in-record
### Version of Awkward Array

main

### Description and code to reproduce

The following example is left unchanged by `ak.fill_none`:
```python
import awkward as ak

record = ak.zip({
    'x': [1, None],
    'y': [2, 3]
})

result = ak.fill_none(record, 0, axis=-1)
result.show()
```

I'm fairly sure that this is a bug, because `ak.is_none` walks successfully through records. My understanding of records is that it's only reducers that cannot peer through them. 

Of course, this example would imply
```python
record = ak.zip({
    "x": [1, 4], 
	"y": [2, 3]
}).mask[[True, False]]
assert ak.fill_none(record, 0, axis=-1).to_list() == [
    {"x": 1, "y": 2},
    0
]
```

I think this is reasonable; again, `is_none` behaves identically to this. Maybe we _do_ want a parameter in future, e.g. `visit_records` or some such.


Discussion:

Resolving patch:
diff --git a/src/awkward/operations/ak_fill_none.py b/src/awkward/operations/ak_fill_none.py
--- a/src/awkward/operations/ak_fill_none.py
+++ b/src/awkward/operations/ak_fill_none.py
@@ -63,7 +63,7 @@ def fill_none(array, value, axis=-1, *, highlevel=True, behavior=None):
 
 def _impl(array, value, axis, highlevel, behavior):
     arraylayout = ak.operations.to_layout(array, allow_record=True, allow_other=False)
-    behavior = ak._util.behavior_of(array, behavior=behavior)
+    behavior = ak._util.behavior_of(array, value, behavior=behavior)
     backend = ak._backends.backend_of(arraylayout, default=cpu)
 
     # Convert value type to appropriate layout
@@ -106,25 +106,24 @@ def _impl(array, value, axis, highlevel, behavior):
             [value], allow_record=False, allow_other=False
         )
 
-    def maybe_fillna(layout):
-        if layout.is_option:
-            return ak._do.fill_none(layout, valuelayout)
-        else:
-            return layout
-
     if axis is None:
 
         def action(layout, continuation, **kwargs):
-            return maybe_fillna(continuation())
+            if layout.is_option:
+                return ak._do.fill_none(continuation(), valuelayout)
 
     else:
 
         def action(layout, depth, **kwargs):
             posaxis = ak._util.maybe_posaxis(layout, axis, depth)
-            if posaxis is not None and posaxis + 1 < depth:
-                return layout
-            elif posaxis is not None and posaxis + 1 == depth:
-                return maybe_fillna(layout)
+            if posaxis is not None and posaxis + 1 == depth:
+                if layout.is_union or layout.is_record:
+                    return None
+                elif layout.is_option:
+                    return ak._do.fill_none(layout, valuelayout)
+                else:
+                    return layout
+
             elif layout.is_leaf:
                 raise ak._errors.wrap_error(
                     np.AxisError(
@@ -133,5 +132,4 @@ def action(layout, depth, **kwargs):
                 )
 
     out = ak._do.recursively_apply(arraylayout, action, behavior)
-
     return ak._util.wrap(out, behavior, highlevel)


Test patch:
diff --git a/tests/test_2064_fill_none_record.py b/tests/test_2064_fill_none_record.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2064_fill_none_record.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_axis_none():
+    record = ak.zip({"x": [1, None], "y": [2, 3]})
+    assert ak.fill_none(record, 0, axis=None).to_list() == [
+        {"x": 1, "y": 2},
+        {"x": 0, "y": 3},
+    ]
+
+
+def test_axis_last():
+    record = ak.zip({"x": [1, None], "y": [2, 3]})
+    assert ak.fill_none(record, 0, axis=-1).to_list() == [
+        {"x": 1, "y": 2},
+        {"x": 0, "y": 3},
+    ]
+
+
+def test_option_outside_record():
+    record = ak.zip({"x": [1, 4], "y": [2, 3]}).mask[[True, False]]
+    assert ak.fill_none(record, 0, axis=-1).to_list() == [{"x": 1, "y": 2}, 0]