Repository: GridTools/gt4py

Issue:
next: np.bool_ argument handling broken in `gtfn` backend
The following test outputs an unexpected error:
```python
@pytest.mark.datatest
def test_bool_fo(icon_grid, backend):
    import gt4py.next as gtx
    from icon4py.model.common.utils import data_allocation
    from icon4py.model.common import field_type_aliases as fa
    from gt4py.next.ffront.fbuiltins import broadcast
    import numpy as np
    @gtx.field_operator
    def bool_fo(val: bool) -> fa.EdgeField[bool]:
        return broadcast(val, (EdgeDim,))
    val = np.bool_(True)
    f = data_allocation.zero_field(icon_grid, dims.EdgeDim, dtype=bool)
    bool_fo.with_backend(backend)(val, out=f, offset_provider={})
```

Error:

```python
>       bool_fo.with_backend(backend)(val, out=f, offset_provider={})

dycore_tests/test_velocity_advection.py:474: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../../.venv/lib/python3.10/site-packages/gt4py/next/ffront/decorator.py:604: in __call__
    return self.backend(
../../../../.venv/lib/python3.10/site-packages/gt4py/next/backend.py:157: in __call__
    self.jit(program, *args, **kwargs)(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

offset_provider = {}
out = NumPyArrayField(_domain=Domain(dims=(Dimension(value='Edge', kind=<DimensionKind.HORIZONTAL: 'horizontal'>),), ranges=(UnitRange(0, 31558),)), _ndarray=array([False, False, False, ..., False, False, False]))
args = (False, NumPyArrayField(_domain=Domain(dims=(Dimension(value='Edge', kind=<DimensionKind.HORIZONTAL: 'horizontal'>),), ranges=(UnitRange(0, 31558),)), _ndarray=array([False, False, False, ..., False, False, False])))
converted_args = [False, (array([False, False, False, ..., False, False, False]), (0,))]

    def decorated_program(
        *args: Any,
        offset_provider: dict[str, common.Connectivity | common.Dimension],
        out: Any = None,
    ) -> None:
        if out is not None:
            args = (*args, out)
        converted_args = [convert_arg(arg) for arg in args]
        conn_args = extract_connectivity_args(offset_provider, device)
        # generate implicit domain size arguments only if necessary, using `iter_size_args()`
>       return inp(
            *converted_args,
            *(arguments.iter_size_args(args) if inp.implicit_domain else ()),
            *conn_args,
        )
E       TypeError: __field_operator_bool_fo(): incompatible function arguments. The following argument types are supported:
E           1. __field_operator_bool_fo(arg0: bool, arg1: tuple[ndarray[dtype=bool, shape=(*)], tuple[int]], arg2: int, /) -> None
E       
E       Invoked with types: bool_, tuple, int

../../../../.venv/lib/python3.10/site-packages/gt4py/next/program_processors/runners/gtfn.py:59: TypeError
```

Discussion:
@havogt: nanobind lacks support for casting `np.bool` to C++ `bool`. See https://github.com/wjakob/nanobind/pull/936.@havogt: Since most likely nanobind doesn't want the ugly np.bool cast, we should just wrap bool args with a `bool()` cast.

Solution patch:
diff --git a/src/gt4py/next/program_processors/runners/gtfn.py b/src/gt4py/next/program_processors/runners/gtfn.py
index 12f5f34a..6bdb19a8 100644
--- a/src/gt4py/next/program_processors/runners/gtfn.py
+++ b/src/gt4py/next/program_processors/runners/gtfn.py
@@ -15,6 +15,7 @@ from typing import Any, Optional
 import diskcache
 import factory
 import filelock
+import numpy as np
 
 import gt4py._core.definitions as core_defs
 import gt4py.next.allocators as next_allocators
@@ -34,6 +35,9 @@ def convert_arg(arg: Any) -> Any:
         arr = arg.ndarray
         origin = getattr(arg, "__gt_origin__", tuple([0] * len(arg.domain)))
         return arr, origin
+    if isinstance(arg, np.bool_):
+        # nanobind does not support implicit conversion of `np.bool` to `bool`
+        return bool(arg)
     else:
         return arg
 


Test patch:
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py
index a042c607..1dbf0ff0 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py
@@ -7,8 +7,10 @@
 # SPDX-License-Identifier: BSD-3-Clause
 
 from functools import reduce
+
 import numpy as np
 import pytest
+
 import gt4py.next as gtx
 from gt4py.next import (
     astype,
@@ -21,9 +23,9 @@ from gt4py.next import (
     int64,
     minimum,
     neighbor_sum,
+    utils as gt_utils,
 )
 from gt4py.next.ffront.experimental import as_offset
-from gt4py.next import utils as gt_utils
 
 from next_tests.integration_tests import cases
 from next_tests.integration_tests.cases import (
@@ -31,6 +33,7 @@ from next_tests.integration_tests.cases import (
     E2V,
     V2E,
     E2VDim,
+    Edge,
     IDim,
     Ioff,
     JDim,
@@ -38,7 +41,6 @@ from next_tests.integration_tests.cases import (
     Koff,
     V2EDim,
     Vertex,
-    Edge,
     cartesian_case,
     unstructured_case,
     unstructured_case_3d,
@@ -196,6 +198,21 @@ def test_scalar_arg(unstructured_case):
     )
 
 
+def test_np_bool_scalar_arg(unstructured_case):
+    """Test scalar argument being turned into 0-dim field."""
+
+    @gtx.field_operator
+    def testee(a: gtx.bool) -> cases.VBoolField:
+        return broadcast(not a, (Vertex,))
+
+    a = np.bool_(True)  # explicitly using a np.bool
+
+    ref = np.full([unstructured_case.default_sizes[Vertex]], not a, dtype=np.bool_)
+    out = cases.allocate(unstructured_case, testee, cases.RETURN)()
+
+    cases.verify(unstructured_case, testee, a, out=out, ref=ref)
+
+
 def test_nested_scalar_arg(unstructured_case):
     @gtx.field_operator
     def testee_inner(a: int32) -> cases.VField: