diff --git a/docs/SPEC.md b/docs/SPEC.md index 358cc8df..4546b566 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -689,7 +689,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads. | Array parameters only indexed, stored, `len`-ed; no aliasing (entry guard) | not compiled; diagnostic | | Integer ops proven safe by the verifier's intervals (constants, `len`, `range` variables, guards such as `if x < e` / `while lo < hi`, joined local assignments, `//` and `%` by constants) | emitted unchecked (a shift/mask for `//`/`%` by a power of two of a non-negative value); the sanitizer CI run builds with `TP_CHECK_PROVEN` and turns a wrong proof into a SystemError (#148) | | `array.array[float]` / `array.array[int]` parameters (exact `array.array`, typecode `d` or `q`/`l`, 8-byte items) are read and written in place: the buffer export is held for the call and released on every exit, and every element access is one relaxed atomic 8-byte load or store, so each access is atomic like the interpreter's `a[i]`; no copy-in and no limit on the number of buffers. Open (#149): while the export is held, another thread's resize of that array raises BufferError. numpy arrays: see the next row | anything else: object path | - | numpy `npt.NDArray[np.float64]` / `NDArray[np.int64]` parameters (exact `numpy.ndarray`, 1-D, C-contiguous, native byte order, writable when stored to) take the same buffer path (#220, step 1) when the function accesses the array purely by element (`x[i]`, `len(x)`); a function that uses it as a whole object (`np.sum(x)`, `x.T`, passing `x` on, a slice) compiles as before with the parameter an opaque object. Other dtypes, including bool, are opaque objects. Results follow the interpreter: a value derived from an element is a numpy scalar there, so it is boxed as `np.float64` / `np.int64` / `np.bool` when it escapes (return, `Box`), decided per value at run time by a taint flag; a function declared `-> float` / `int` / `bool` that holds a numpy array returns the interpreter's object. `float(v)` of such a value is a plain float. An out-of-range index raises numpy's `IndexError("index i is out of bounds for axis 0 with size n")`. Arithmetic on a value derived from an element follows numpy's scalar semantics (#220, step 2): float `/`, `//`, `%` never raise ZeroDivisionError (inf / nan, with numpy's result bits and sign rules, `//` and `%` as numpy's `divmod`), int64 `//` and `%` by zero give 0, int64 `/` converts both operands to double. numpy's RuntimeWarning (category and text exactly as numpy's, e.g. `overflow encountered in scalar add`, `divide by zero encountered in scalar divide`, `invalid value encountered in scalar floor_divide`) is emitted once per flagged operation, with the source file and line of the operation. A pure function does not emit it itself: on any floating-point exception (overflow, invalid, divide by zero) or int64 wrap or division by zero it deopts, so the interpreter emits it; an impure function cannot deopt after an effect, so the compiled code emits the warning (`PyErr_WarnExplicitObject`, honouring `warnings` filters and the module's `__warningregistry__`). Float `+ - * / // %` on such values is compiled in both kinds of function, including a store of the result into the array (`x[i] -= d * m`); int64 `/ // %` is compiled in a pure function only. Int64 `+ - *` and `// %` of an element in an impure function (a native INT would have to be a numpy scalar) stay on the object path | anything else: object path | + | numpy `npt.NDArray[np.float64]` / `NDArray[np.int64]` parameters (exact `numpy.ndarray`, 1-D, C-contiguous, native byte order, writable when stored to) take the same buffer path (#220, step 1) when the function accesses the array purely by element (`x[i]`, `len(x)`); a function that uses it as a whole object (`np.sum(x)`, `x.T`, passing `x` on, a slice) compiles as before with the parameter an opaque object. `NDArray[np.bool_]` (#261) is the same path with 1-byte items (buffer format `?`; a `uint8` or other array deopts): an element read is a native bool, boxed as `np.bool` when it escapes; a store compiles only for a bool value (`x[i] = v`, `v: bool`), because numpy converts an int or float by truthiness and that function stays interpreted. Other dtypes are opaque objects. Results follow the interpreter: a value derived from an element is a numpy scalar there, so it is boxed as `np.float64` / `np.int64` / `np.bool` when it escapes (return, `Box`), decided per value at run time by a taint flag; a function declared `-> float` / `int` / `bool` that holds a numpy array returns the interpreter's object. `float(v)` of such a value is a plain float. An out-of-range index raises numpy's `IndexError("index i is out of bounds for axis 0 with size n")`. Arithmetic on a value derived from an element follows numpy's scalar semantics (#220, step 2): float `/`, `//`, `%` never raise ZeroDivisionError (inf / nan, with numpy's result bits and sign rules, `//` and `%` as numpy's `divmod`), int64 `//` and `%` by zero give 0, int64 `/` converts both operands to double. numpy's RuntimeWarning (category and text exactly as numpy's, e.g. `overflow encountered in scalar add`, `divide by zero encountered in scalar divide`, `invalid value encountered in scalar floor_divide`) is emitted once per flagged operation, with the source file and line of the operation. A pure function does not emit it itself: on any floating-point exception (overflow, invalid, divide by zero) or int64 wrap or division by zero it deopts, so the interpreter emits it; an impure function cannot deopt after an effect, so the compiled code emits the warning (`PyErr_WarnExplicitObject`, honouring `warnings` filters and the module's `__warningregistry__`). Float `+ - * / // %` on such values is compiled in both kinds of function, including a store of the result into the array (`x[i] -= d * m`); int64 `/ // %` is compiled in a pure function only. Int64 `+ - *` and `// %` of an element in an impure function (a native INT would have to be a numpy scalar) stay on the object path | anything else: object path | | A function holding an array parameter runs no user code: closed, and no slot read or `C(args)` whose slow path runs user code, in its whole call tree (#203) | not compiled; diagnostic | | OBJ references follow one ownership rule; no IR form for free, borrowed alias or NULL read | not compiled; diagnostic | | Integer overflow checked and promoted (deopt); an op marked `proven` must re-prove its interval fits i64 | not compiled; diagnostic `verify/proven-overflow` | diff --git a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py index 22cd4082..63238fb9 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py @@ -188,10 +188,10 @@ class CGenError(Exception): _BUFFER_KINDS = {None: None, "array": 0, "numpy": 1} # buf_types slots 2..4: the numpy scalar types a Box.np makes (np.float64, np.int64, np.bool_) _NP_SLOT = {Type.F64: 2, Type.I64: 3, Type.BOOL: 4} -_BUFFER_FMT = {Type.F64_ARRAY: ('"d"', "NULL"), Type.I64_ARRAY: ('"q"', '"l"')} -_BUFFER_SFX = {Type.F64_ARRAY: "f64", Type.I64_ARRAY: "i64"} +_BUFFER_FMT = {Type.F64_ARRAY: ('"d"', "NULL"), Type.I64_ARRAY: ('"q"', '"l"'), Type.BOOL_ARRAY: ('"?"', "NULL")} +_BUFFER_SFX = {Type.F64_ARRAY: "f64", Type.I64_ARRAY: "i64", Type.BOOL_ARRAY: "bool"} _CT_ARRAY_REV = {"tp_f64_array": Type.F64_ARRAY, "tp_i64_array": Type.I64_ARRAY} -_ARRAY_ELEM = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64} +_ARRAY_ELEM = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64, Type.BOOL_ARRAY: Type.BOOL} _ARRAY_PFX = {Type.F64_ARRAY: "tp_f64_array", Type.I64_ARRAY: "tp_i64_array"} _UNBOX = {Type.I64: "tp_unbox_i64", Type.F64: "tp_unbox_f64", Type.BOOL: "tp_unbox_bool"} _BOX = {Type.I64: "tp_box_i64", Type.F64: "tp_box_f64", Type.BOOL: "tp_box_bool"} diff --git a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py index a61478d8..a5314a03 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py @@ -93,7 +93,7 @@ TYPE_NAME = { Type.I64: "int", Type.F64: "float", Type.BOOL: "bool", Type.NONE: "None", Type.OBJ: "object", Type.INT: "int", - Type.F64_ARRAY: "list[float]", Type.I64_ARRAY: "list[int]", + Type.F64_ARRAY: "list[float]", Type.I64_ARRAY: "list[int]", Type.BOOL_ARRAY: "NDArray[np.bool_]", } SCALAR_KIND = {"int": Type.I64, "float": Type.F64, "bool": Type.BOOL} ARRAY_KIND = {"list[float]": Type.F64_ARRAY, "list[int]": Type.I64_ARRAY} @@ -341,7 +341,8 @@ def _element_only(fn: ast.FunctionDef, name: str) -> bool: # numpy dtypes that are 8-byte buffer arrays (#220): the scalar type annotation they spell -_NUMPY_DTYPE = {"numpy.float64": Type.F64_ARRAY, "numpy.int64": Type.I64_ARRAY} +_NUMPY_DTYPE = {"numpy.float64": Type.F64_ARRAY, "numpy.int64": Type.I64_ARRAY, + "numpy.bool_": Type.BOOL_ARRAY, "numpy.bool": Type.BOOL_ARRAY} def _scalar_globals(tree: ast.Module, bindings: dict[str, list[tuple[int, str]]]) -> dict[str, Type]: @@ -801,7 +802,8 @@ def _numpy_kinds(sig: _Signature, inferred: dict[str, list[tuple[int, str]]], nodes = {n.lineno: n for n in ast.walk(sig.node) if isinstance(n, (ast.Assign, ast.AugAssign)) and len(getattr(n, "targets", [0])) == 1} - out = {name: [(line, _NUMPY_SPELLED.get(k, k)) for line, k in entries] + # newer numpy stubs type an element `typing.Any`: the same "source settles it" case as `Unknown` + out = {name: [(line, _NUMPY_SPELLED.get(k, "Unknown" if k == "typing.Any" else k)) for line, k in entries] for name, entries in inferred.items()} def known(name: str) -> str | None: @@ -816,7 +818,7 @@ def kind(e: ast.expr) -> str | None: if isinstance(e, ast.Name): return known(e.id) if isinstance(e, ast.Subscript) and isinstance(e.value, ast.Name) and e.value.id in arrays: - return "float" if arrays[e.value.id] is Type.F64_ARRAY else "int" + return {Type.F64_ARRAY: "float", Type.BOOL_ARRAY: "bool"}.get(arrays[e.value.id], "int") if isinstance(e, ast.UnaryOp): return "bool" if isinstance(e.op, ast.Not) else kind(e.operand) if isinstance(e, ast.BinOp): @@ -1800,7 +1802,7 @@ def array_of(self, node: ast.Subscript) -> tuple[str, Type]: if not (isinstance(node.value, ast.Name) and node.value.id in self.arrays): raise Skip(node.lineno, f"subscript `{_src(node)}` of an object is not supported") name = node.value.id - return name, Type.F64 if self.arrays[name] == Type.F64_ARRAY else Type.I64 + return name, {Type.F64_ARRAY: Type.F64, Type.BOOL_ARRAY: Type.BOOL}.get(self.arrays[name], Type.I64) def index(self, node: ast.expr) -> ir.Expr: if isinstance(node, ast.Slice): diff --git a/python-multiplatform-ksp/src/main/python/typedpython/ir.py b/python-multiplatform-ksp/src/main/python/typedpython/ir.py index 8705b8fe..e6470808 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/ir.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/ir.py @@ -68,12 +68,13 @@ class Type(Enum): INT = "int" # a Python int that is a native i64 or, once promoted, a PyLong (SPEC N-13) F64_ARRAY = "f64[]" # a list[float] parameter (ArrayParam) or a local array (NewArray/CopyArray) I64_ARRAY = "i64[]" # a list[int] parameter (ArrayParam) or a local array (NewArray/CopyArray) + BOOL_ARRAY = "bool[]" # a numpy np.bool_ buffer parameter only (1-byte items, #261) SCALARS = (Type.I64, Type.F64, Type.BOOL) # INT is not a scalar: it owns a reference when promoted. It is created only by `ToInt`, by arithmetic # on INTs, and by a redo `Call` of an i64-returning callee, and it becomes an object only through `Box`. -ARRAYS = (Type.F64_ARRAY, Type.I64_ARRAY) +ARRAYS = (Type.F64_ARRAY, Type.I64_ARRAY, Type.BOOL_ARRAY) # --- expressions --------------------------------------------------------------------------------- diff --git a/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h b/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h index 25aaea17..881f7861 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h +++ b/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h @@ -1115,8 +1115,8 @@ static inline void tp_buf_exit(tp_buf *b) memset(b, 0, sizeof(*b)); } -static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2, - int writable, tp_buf *b) +static inline int tp__buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2, + int writable, Py_ssize_t isz, tp_buf *b) { int flags = PyBUF_C_CONTIGUOUS | PyBUF_FORMAT; memset(b, 0, sizeof(*b)); @@ -1135,12 +1135,12 @@ static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, return 1; } b->held = 1; - if (b->view.ndim != 1 || b->view.itemsize != 8 || b->view.shape == NULL + if (b->view.ndim != 1 || b->view.itemsize != isz || b->view.shape == NULL || b->view.suboffsets != NULL || b->view.shape[0] < 0 - || b->view.shape[0] * 8 != b->view.len || b->view.format == NULL + || b->view.shape[0] * isz != b->view.len || b->view.format == NULL || !(strcmp(b->view.format, fmt1) == 0 || (fmt2 != NULL && strcmp(b->view.format, fmt2) == 0)) || (writable && b->view.readonly) - || (b->view.shape[0] > 0 && (b->view.buf == NULL || ((uintptr_t)b->view.buf & 7) != 0))) { + || (b->view.shape[0] > 0 && (b->view.buf == NULL || ((uintptr_t)b->view.buf & (uintptr_t)(isz - 1)) != 0))) { tp_buf_exit(b); return 1; } @@ -1149,6 +1149,13 @@ static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, return 0; } +/* The item size follows the format: "?" (numpy bool_, #261) has 1-byte items, every other format 8. */ +static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2, + int writable, tp_buf *b) +{ + return tp__buf_enter(obj, exact, fmt1, fmt2, writable, strcmp(fmt1, "?") == 0 ? 1 : 8, b); +} + /* A numpy array (#220): the same guard on the exact `numpy.ndarray`; `np` selects numpy's IndexError * text. The format check already refuses a byte-order prefix and every dtype but float64 / int64. */ static inline int tp_buf_enter_np(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2, @@ -1191,7 +1198,19 @@ static inline double tp_buf_ld_f64(const tp_buf *b, Py_ssize_t i) { return ((con static inline int64_t tp_buf_ld_i64(const tp_buf *b, Py_ssize_t i) { return ((const int64_t *)b->data)[i]; } static inline void tp_buf_st_f64(const tp_buf *b, Py_ssize_t i, double v) { ((double *)b->data)[i] = v; } static inline void tp_buf_st_i64(const tp_buf *b, Py_ssize_t i, int64_t v) { ((int64_t *)b->data)[i] = v; } +static inline int tp_buf_ld_bool(const tp_buf *b, Py_ssize_t i) { return ((const uint8_t *)b->data)[i] != 0; } +static inline void tp_buf_st_bool(const tp_buf *b, Py_ssize_t i, int v) { ((uint8_t *)b->data)[i] = v != 0; } #else +static inline int tp_buf_ld_bool(const tp_buf *b, Py_ssize_t i) +{ + return __atomic_load_n((const uint8_t *)b->data + i, __ATOMIC_RELAXED) != 0; +} + +static inline void tp_buf_st_bool(const tp_buf *b, Py_ssize_t i, int v) +{ + __atomic_store_n((uint8_t *)b->data + i, (uint8_t)(v != 0), __ATOMIC_RELAXED); +} + static inline double tp_buf_ld_f64(const tp_buf *b, Py_ssize_t i) { uint64_t u = __atomic_load_n((const uint64_t *)b->data + i, __ATOMIC_RELAXED); diff --git a/python-multiplatform-ksp/src/main/python/typedpython/verify.py b/python-multiplatform-ksp/src/main/python/typedpython/verify.py index 0b5f90b1..c8126ed3 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/verify.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/verify.py @@ -299,7 +299,7 @@ class Diagnostic: _INT_PRODUCERS = frozenset({ToInt, BinOp, UnaryOp, Local, Call}) _INT_OPS = frozenset({BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL, BinOpKind.FLOORDIV, BinOpKind.MOD, BinOpKind.LSHIFT, BinOpKind.RSHIFT}) -_ELEMENT = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64} +_ELEMENT = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64, Type.BOOL_ARRAY: Type.BOOL} _MATH_ARITY = {MathFunc.ATAN2: 2, MathFunc.HYPOT: 2} # every other MathFunc: 1 _I64_DEOPT_OPS = frozenset({BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL, BinOpKind.FLOORDIV, BinOpKind.MOD, BinOpKind.TRUEDIV}) diff --git a/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py b/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py index b94a9f58..582e4b09 100644 --- a/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py +++ b/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py @@ -356,6 +356,67 @@ def nd_energy_full(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], dz: float = z[i] - z[j] e -= mass[i] * mass[j] / math.sqrt(dx * dx + dy * dy + dz * dz) return e + # #261: np.bool_ arrays (1-byte items, format "?") + def nd_b_get(x: npt.NDArray[np.bool_], i: int) -> object: + return x[i] + + + def nd_b_any(x: npt.NDArray[np.bool_]) -> bool: + for i in range(len(x)): + if x[i]: + return True + return False + + + def nd_b_count(x: npt.NDArray[np.bool_]) -> int: + c = 0 + for i in range(len(x)): + if x[i]: + c += 1 + return c + + + def nd_b_set(x: npt.NDArray[np.bool_], i: int, v: bool) -> None: + x[i] = v + + + def nd_b_flip(x: npt.NDArray[np.bool_]) -> None: + for i in range(len(x)): + x[i] = not x[i] + + + def nd_b_copy(a: npt.NDArray[np.bool_], b: npt.NDArray[np.bool_]) -> None: + for i in range(len(a)): + b[i] = a[i] + + + def nd_b_eq(x: npt.NDArray[np.bool_], y: npt.NDArray[np.bool_], i: int) -> object: + return x[i] == y[i] + + + def nd_b_gt(x: npt.NDArray[np.float64], m: npt.NDArray[np.bool_]) -> None: + for i in range(len(x)): + m[i] = x[i] > 2.0 + + + def nd_b_len(x: npt.NDArray[np.bool_]) -> int: + return len(x) + + + def nd_b_store_int(x: npt.NDArray[np.bool_], i: int, v: int) -> None: + x[i] = v + + + def nd_b_store_float(x: npt.NDArray[np.bool_], i: int, v: float) -> None: + x[i] = v + + + def nd_b_arith(x: npt.NDArray[np.bool_], i: int) -> object: + return x[i] + 1 + + + def nd_b_whole(x: npt.NDArray[np.bool_]) -> object: + return np.sum(x) ''') COMPILED = { @@ -365,11 +426,13 @@ def nd_energy_full(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], # step 2 "nd_div", "nd_idiv", "nd_scale", "p_div", "p_fdiv", "p_mod", "p_add", "p_sub", "p_mul", "p_pdiv", "p_idiv", "p_ifdiv", "p_imod", "p_imixed", "i_div", "i_fdiv", "i_mod", "i_add", "i_sub", "i_mul", + "nd_b_get", "nd_b_any", "nd_b_count", "nd_b_set", "nd_b_flip", "nd_b_copy", "nd_b_eq", "nd_b_gt", + "nd_b_len", "i_plain", "i_multiline", "i_aug", "i_loop", "i_mixed", "i_ret", "nd_advance", "nd_energy_full", } IMPURE = {"i_div", "i_fdiv", "i_mod", "i_add", "i_sub", "i_mul", "i_plain", "i_multiline", "i_aug", "i_loop", "i_mixed", "i_ret", "nd_advance", "nd_scale"} -NOT_COMPILED = {"i_ifdiv", "nd_to_list", "nd_mixed_cmp", "nd_abs", "nd_int_impure"} # nd_abs calls user code # could warn / divide, or store a possibly warning op +NOT_COMPILED = {"nd_b_store_int", "nd_b_store_float", "nd_b_arith", "i_ifdiv", "nd_to_list", "nd_mixed_cmp", "nd_abs", "nd_int_impure"} # nd_abs calls user code # could warn / divide, or store a possibly warning op F = [1.5, -2.25, 3.0, 4.5] I = [3, -5, 7, 11] @@ -806,3 +869,87 @@ def test_nbody_advance_and_energy_compile_and_match(built): ia, iw = run(ref["nd_advance"], (0.01, 1, *b)) assert cw == iw and cw, (cw, iw) assert all(same(p, q) for u, v in zip(a, b) for p, q in zip(u.tolist(), v.tolist())) + + +# --- #261: bool arrays (1-byte elements) --------------------------------------------------------------- + +def bl(v=(True, False, True, True)): + return np.array(v, dtype=np.bool_) + + +def test_bool_array_params_are_numpy_buffers_of_one_byte_items(built): + proved, _, c = built + by = {f.name: f for f in proved.functions} + assert by["nd_b_get"].params[0].buffer == "numpy" + assert by["nd_b_get"].params[0].type is ir.Type.BOOL_ARRAY + assert by["nd_b_set"].params[0].stored + assert '"?"' in c + + +def test_bool_element_is_np_bool(built): + _, m, _ = built + for i in (0, 1, -1): + c, _ = differential(m, "nd_b_get", lambda: (bl(), i)) + assert type(c[1]) is np.bool_ + + +def test_bool_loops_and_results_match(built): + _, m, _ = built + for v in ([], [False], [False, False], [True, False, True], [False] * 9 + [True]): + c, _ = differential(m, "nd_b_any", lambda: (bl(v),)) + assert type(c[1]) is bool + c, _ = differential(m, "nd_b_count", lambda: (bl(v),)) + assert type(c[1]) is int + differential(m, "nd_b_len", lambda: (bl(v),)) + differential(m, "nd_b_flip", lambda: (bl(v),)) + differential(m, "nd_b_copy", lambda: (bl(), bl([False] * 4))) + + +def test_bool_stores_and_negative_indexes(built): + _, m, _ = built + for i in (0, 3, -1, -4): + for v in (True, False): + differential(m, "nd_b_set", lambda: (bl(), i, v)) + differential(m, "nd_b_eq", lambda: (bl(), bl([True, True, False, True]), 1)) + c, _ = differential(m, "nd_b_eq", lambda: (bl(), bl(), 0)) + assert type(c[1]) is np.bool_ + differential(m, "nd_b_gt", lambda: (f64(), bl([False] * 4))) + + +@pytest.mark.parametrize("i", [4, -5, 100]) +def test_bool_out_of_range_has_numpys_message(built, i): + _, m, _ = built + c, _ = differential(m, "nd_b_get", lambda: (bl(), i)) + assert c[0] == "raise" and c[1] is IndexError and "out of bounds for axis 0 with size 4" in c[2] + c, _ = differential(m, "nd_b_set", lambda: (bl(), i, True)) + assert c[0] == "raise" and "out of bounds for axis 0 with size 4" in c[2] + + +def test_a_store_numpy_converts_differently_stays_interpreted(built): + proved, m, _ = built + for n in ("nd_b_store_int", "nd_b_store_float", "nd_b_arith"): + assert n in proved.skipped + for v in (0, 1, 5, -3, 2 ** 70): + differential(m, "nd_b_store_int", lambda: (bl(), 1, v)) + for v in (0.0, 0.5, float("nan")): + differential(m, "nd_b_store_float", lambda: (bl(), 2, v)) + differential(m, "nd_b_arith", lambda: (bl(), 0)) + + +def test_bool_guard_refuses_other_dtypes_and_views(built): + _, m, _ = built + differential(m, "nd_b_get", lambda: (bl(), 0)) + differential(m, "nd_b_get", lambda: (np.array([1, 0], dtype=np.uint8), 0), expect_deopt=True) + differential(m, "nd_b_get", lambda: (f64(), 0), expect_deopt=True) + differential(m, "nd_b_get", lambda: (np.array([True, False, True, False])[::2], 0), expect_deopt=True) + differential(m, "nd_first", lambda: (bl(),), expect_deopt=True) # a float64 function, bool array + ro = bl() + ro.setflags(write=False) + differential(m, "nd_b_set", lambda: (ro, 0, True), expect_deopt=True) + + +def test_bool_whole_array_use_is_an_opaque_object(built): + proved, m, _ = built + f = next((f for f in proved.functions if f.name == "nd_b_whole"), None) + assert f is not None and not any(isinstance(p, ir.ArrayParam) for p in f.params) + differential(m, "nd_b_whole", lambda: (bl(),))