diff --git a/graphblas/core/dtypes.py b/graphblas/core/dtypes.py index a5dddd017..1dd52ecce 100644 --- a/graphblas/core/dtypes.py +++ b/graphblas/core/dtypes.py @@ -190,16 +190,32 @@ def _set_udt_jit_c_definition(datatype): } +def _iter_leaves(np_type): + """Yield each leaf dtype of ``np_type``: a scalar, or an array of scalars, shape and all. + + Walks records, nested records, arrays of records and arrays of arrays + (numpy keeps ``np.dtype((np.dtype(("?", (2,))), (3,)))`` as it is given), + in field order, so it reaches every value an element holds. A leaf inside + an array of records or of arrays comes once, as its record or inner array + declares it. ``udt._iter_record_leaves``, which pairs two records' leaves + for an op, yields an array of records as one leaf. + """ + if np_type.names is not None: + for name in np_type.names: + yield from _iter_leaves(np_type.fields[name][0]) + elif np_type.subdtype is not None and ( + np_type.subdtype[0].names is not None or np_type.subdtype[0].subdtype is not None + ): + yield from _iter_leaves(np_type.subdtype[0]) + else: + yield np_type + + def _numba_untyped_array_element(dtype): """The kind of the first array element in dtype that Numba cannot type, or None.""" - if dtype.subdtype is not None: - base = dtype.subdtype[0] - if base.kind in _NUMBA_UNTYPED_ARRAY_ELEMENTS: - return base.kind - return _numba_untyped_array_element(base) - for field_dtype, *_ in (dtype.fields or {}).values(): - if (kind := _numba_untyped_array_element(field_dtype)) is not None: - return kind + for leaf in _iter_leaves(dtype): + if leaf.subdtype is not None and leaf.base.kind in _NUMBA_UNTYPED_ARRAY_ELEMENTS: + return leaf.base.kind return None @@ -230,14 +246,7 @@ def _numba_type(dtype): def _raw_bytes_element(dtype): """The first void dtype with no fields (raw bytes) in dtype, or None.""" - if dtype.subdtype is not None: - return _raw_bytes_element(dtype.subdtype[0]) - if dtype.names is None: - return dtype if dtype.kind == "V" else None - for field_dtype, *_ in dtype.fields.values(): - if (raw := _raw_bytes_element(field_dtype)) is not None: - return raw - return None + return next((leaf.base for leaf in _iter_leaves(dtype) if leaf.base.kind == "V"), None) def register_new(name, dtype=None): @@ -672,11 +681,12 @@ def unify(type1, type2, *, is_left_scalar=False, is_right_scalar=False): unify(BOOL, UINT16) -> UINT16 unify(FP32, INT32) -> FP64 - ``is_left_scalar`` and ``is_right_scalar`` no longer change the result. A - Python number is typed before it gets here (``literals._literal_operand``), and - a typed scalar promotes as any operand does, as numpy 2 promotes it. They - used to stand the scalar in as a 0-d array of value 0, which numpy 1 typed - by value, so ``int8_vec + 300`` wrapped there. + ``is_left_scalar`` and ``is_right_scalar`` are accepted and ignored, for code + that still passes them; python-graphblas itself no longer does. A Python + number is typed before it gets here (``literals._literal_operand``), and a + typed scalar promotes as any operand does, as numpy 2 promotes it. They used + to stand the scalar in as a 0-d array of value 0, which numpy 1 typed by + value, so ``int8_vec + 300`` wrapped there. """ if type1 is type2: return type1 @@ -785,11 +795,7 @@ def _nats_in_leaf_units(values, np_type): def _has_duration_leaf(np_type): """Whether ``np_type`` has a timedelta64 leaf: a record field, an array's element, or itself.""" - if np_type.subdtype is not None: - return _has_duration_leaf(np_type.subdtype[0]) - if np_type.names is not None: - return any(_has_duration_leaf(np_type.fields[name][0]) for name in np_type.names) - return np_type.kind == "m" + return any(leaf.base.kind == "m" for leaf in _iter_leaves(np_type)) def _dtype_to_string(dtype): diff --git a/graphblas/core/literals.py b/graphblas/core/literals.py index 8945826ef..0c1cbdbc1 100644 --- a/graphblas/core/literals.py +++ b/graphblas/core/literals.py @@ -126,10 +126,10 @@ def _literal_operand(dtype, value, op, *, left=False): parts = _as_record_parts(np_type, value) # First: making an int beyond its leaf infinity reshapes the part. _refuse_misfit_fields(dtype, np_type, parts) - if _int_beyond_leaf(np_type, parts) is not None: + if (infinite := _beyond_as_infinity(np_type, parts)) is not None: # As infinity in its leaf, it still equals no element, and it # stays out of an array numpy makes of numpy values beside it. - value = _beyond_as_infinity(np_type, parts) + value = infinite literal_type = _literal_type(dtype, value, exact=exact) if literal_type is not None: return value, literal_type @@ -828,56 +828,45 @@ def _refuse_misfit_fields(dtype, np_type, value): ) -def _int_beyond_leaf(np_type, value): - """Return a Python int in literal ``value`` beyond its integer leaf's range, or ``None``. +def _beyond_as_infinity(np_type, value): + """Return literal ``value`` with each int beyond its integer leaf's range as infinity. ``value`` is laid out like an element of ``np_type``, as :func:`_weak_literal_udt` reads it (a record's dict already a tuple). No - element equals a literal with such an int, whatever its other leaves. + element equals a literal with such an int, whatever its other leaves, and + infinity of the int's sign equals no element either. Every other value is + kept, so a text leaf still pairs with its field. Return ``None`` if + ``value`` holds no such int. """ - if np_type.names is not None: - parts = value if isinstance(value, (tuple, list)) else [value] * len(np_type.names) - if len(parts) != len(np_type.names): - return None # not laid out like an element; typing it reports that - for name, part in zip(np_type.names, parts, strict=True): - found = _int_beyond_leaf(np_type.fields[name][0], part) - if found is not None: - return found - return None - base, shape = np_type.subdtype or (np_type, ()) - if base.kind in "iu": - lo, hi = _int_bounds(base) - for x in _literal_leaf_values(value, shape, numpy_scalars=True) or (): - if type(x) is int and not lo <= x <= hi: - return x - return None - - -def _beyond_as_infinity(np_type, value): - """Return literal ``value`` with each int beyond its integer leaf's range as infinity. + found = False - ``value`` is laid out as :func:`_int_beyond_leaf` reads it. Infinity of the - int's sign equals no element, as the int does; every other value is kept, - so a text leaf still pairs with its field. - """ - if np_type.names is not None: - parts = value if isinstance(value, (tuple, list)) else [value] * len(np_type.names) - if len(parts) != len(np_type.names): - return value - return tuple( - _beyond_as_infinity(np_type.fields[name][0], part) - for name, part in zip(np_type.names, parts, strict=True) + def walk(np_type, value): + nonlocal found + if np_type.names is not None: + parts = value if isinstance(value, (tuple, list)) else [value] * len(np_type.names) + if len(parts) != len(np_type.names): + return value # not laid out like an element; typing it reports that + return tuple( + walk(np_type.fields[name][0], part) + for name, part in zip(np_type.names, parts, strict=True) + ) + base, shape = np_type.subdtype or (np_type, ()) + values = ( + _literal_leaf_values(value, shape, numpy_scalars=True) if base.kind in "iu" else None ) - base, shape = np_type.subdtype or (np_type, ()) - values = _literal_leaf_values(value, shape, numpy_scalars=True) if base.kind in "iu" else None - if values is None: - return value - lo, hi = _int_bounds(base) - values = [ - (math.inf if x > 0 else -math.inf) if type(x) is int and not lo <= x <= hi else x - for x in values - ] - return np.array(values, dtype=object).reshape(shape).tolist() if shape else values[0] + if values is None: + return value + lo, hi = _int_bounds(base) + beyond = [type(x) is int and not lo <= x <= hi for x in values] + found = found or any(beyond) + values = [ + (math.inf if x > 0 else -math.inf) if is_beyond else x + for x, is_beyond in zip(values, beyond, strict=True) + ] + return np.array(values, dtype=object).reshape(shape).tolist() if shape else values[0] + + value = walk(np_type, value) + return value if found else None def _array_literal_udt(dtype, value): diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index c14ba79ec..4a62c09c9 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -1749,7 +1749,7 @@ def to_dense(self, fill_value=None, dtype=None, **opts): keyword_name="fill_value", extra_message="Literal scalars also accepted.", ) - dtype = unify(fill_value.dtype, self.dtype, is_left_scalar=True) + dtype = unify(fill_value.dtype, self.dtype) rv = self.dup(dtype, clear=True, name="to_dense", **opts) rv(**opts) << fill_value @@ -2354,8 +2354,8 @@ def _ewise_union(self, other, op, left_default, right_default, is_infix=False): op1 = get_typed_op(op, self.dtype, right.dtype, kind="binary") op2 = get_typed_op(op, left.dtype, other.dtype, kind="binary") if op1 is not op2: - left_dtype = unify(op1.type, op2.type, is_right_scalar=True) - right_dtype = unify(op1.type2, op2.type2, is_left_scalar=True) + left_dtype = unify(op1.type, op2.type) + right_dtype = unify(op1.type2, op2.type2) op = get_typed_op(op, left_dtype, right_dtype, kind="binary") else: op = op1 @@ -2669,7 +2669,7 @@ def apply(self, op, right=None, *, left=None): extra_message="Literal scalars also accepted.", op=op, ) - op = get_typed_op(op, left.dtype, self.dtype, is_left_scalar=True, kind="binary") + op = get_typed_op(op, left.dtype, self.dtype, kind="binary") if opclass == "Monoid": op = op.binaryop else: @@ -2707,12 +2707,10 @@ def apply(self, op, right=None, *, left=None): op=op, ) if opclass in {"IndexUnaryOp", "SelectOp"}: - op = get_typed_op( - op, self.dtype, right.dtype, is_right_scalar=True, kind="indexunary" - ) + op = get_typed_op(op, self.dtype, right.dtype, kind="indexunary") cfunc_method = "IndexOp" else: - op = get_typed_op(op, self.dtype, right.dtype, is_right_scalar=True, kind="binary") + op = get_typed_op(op, self.dtype, right.dtype, kind="binary") cfunc_method = "BinaryOp2nd" if opclass == "Monoid": op = op.binaryop @@ -2843,7 +2841,7 @@ def select(self, op, thunk=None): ncols=self._ncols, dtype=self.dtype, ) - op = get_typed_op(op, self.dtype, thunk.dtype, is_right_scalar=True, kind="select") + op = get_typed_op(op, self.dtype, thunk.dtype, kind="select") op, thunk = _value_select_in_own_type(op, self.dtype, thunk, given) self._expect_op(op, ("SelectOp", "IndexUnaryOp"), within=method_name, argname="op") if thunk._is_cscalar: diff --git a/graphblas/core/operator/utils.py b/graphblas/core/operator/utils.py index 1433d2d4d..3b6c211fa 100644 --- a/graphblas/core/operator/utils.py +++ b/graphblas/core/operator/utils.py @@ -90,7 +90,7 @@ def _register_aggregator_types(aggregator, typed_aggregator): _TypedAggregator = typed_aggregator -def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scalar=False, kind=None): +def get_typed_op(op, dtype, dtype2=None, *, kind=None): if isinstance(op, OpBase): # UDTs always get compiled if op._is_udt: @@ -122,9 +122,7 @@ def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scala return op._compile_udt(dtype, dtype2) # Generic case: try to unify the two dtypes try: - return op[ - unify(dtype, dtype2, is_left_scalar=is_left_scalar, is_right_scalar=is_right_scalar) - ] + return op[unify(dtype, dtype2)] # Two built-in types always unify, and a UDT beside any other type returned above. except (TypeError, AttributeError): # pragma: no cover (safety) # Failure to unify implies a dtype is UDT; some builtin operators can handle UDTs @@ -135,14 +133,7 @@ def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scala return op._compile_udt(dtype, dtype2) if isinstance(op, ParameterizedUdf): op = op() # Use default parameters of parameterized UDFs - return get_typed_op( - op, - dtype, - dtype2, - is_left_scalar=is_left_scalar, - is_right_scalar=is_right_scalar, - kind=kind, - ) + return get_typed_op(op, dtype, dtype2, kind=kind) if isinstance(op, TypedOpBase): return op @@ -182,14 +173,7 @@ def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scala '"unary", "binary", "monoid", "semiring", "indexunary", "select", ' 'or "binary|aggregator".' ) - return get_typed_op( - op, - dtype, - dtype2, - is_left_scalar=is_left_scalar, - is_right_scalar=is_right_scalar, - kind=kind, - ) + return get_typed_op(op, dtype, dtype2, kind=kind) if isinstance(op, FunctionType): if op.__module__ in {"graphblas.indexunary", "graphblas.select"}: # Not a UDF: a helper such as ``indexunary.value`` or a module function @@ -206,14 +190,7 @@ def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scala op = BinaryOp.register_anonymous(op, is_udt=True) return op._compile_udt(dtype, dtype2) if isinstance(op, BuiltinFunctionType) and op in _builtin_to_op: - return get_typed_op( - _builtin_to_op[op], - dtype, - dtype2, - is_left_scalar=is_left_scalar, - is_right_scalar=is_right_scalar, - kind=kind, - ) + return get_typed_op(_builtin_to_op[op], dtype, dtype2, kind=kind) raise TypeError(f"Unable to get typed operator from object with type {type(op)}") @@ -231,14 +208,7 @@ def _get_typed_op_from_exprs(op, left, right, *, kind=None): right_dtype = right_op.type2 else: right_dtype = right.dtype - return get_typed_op( - op, - left_dtype, - right_dtype, - is_left_scalar=left._is_scalar, - is_right_scalar=right._is_scalar, - kind=kind, - ) + return get_typed_op(op, left_dtype, right_dtype, kind=kind) def get_semiring(monoid, binaryop, name=None): diff --git a/graphblas/core/scalar.py b/graphblas/core/scalar.py index cabbde48c..fff06a1d0 100644 --- a/graphblas/core/scalar.py +++ b/graphblas/core/scalar.py @@ -333,25 +333,11 @@ def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False): if not _has_numba: # Check if types are compatible, and compare in the type they unify to, as # with Numba (INT64 with UINT64 is in FP64). Python ints do not wrap in x - y. - eq = get_typed_op( - binary.eq, - self.dtype, - other.dtype, - is_left_scalar=True, - is_right_scalar=True, - kind="binary", - ) + eq = get_typed_op(binary.eq, self.dtype, other.dtype, kind="binary") x = eq.type.np_type.type(self.value).item() y = eq.type2.np_type.type(other.value).item() return isclose_func(x, y) - isclose_func = get_typed_op( - isclose_func, - self.dtype, - other.dtype, - is_left_scalar=True, - is_right_scalar=True, - kind="binary", - ) + isclose_func = get_typed_op(isclose_func, self.dtype, other.dtype, kind="binary") # Pass the values in the op's input types, as ewise_mult does; Numba types a # Python int as int64, which a UINT64 value need not fit x = isclose_func.type.np_type.type(self.value) @@ -867,8 +853,8 @@ def _ewise_union(self, other, op, left_default, right_default, is_infix=False): op1 = get_typed_op(op, self.dtype, right.dtype, kind="binary") op2 = get_typed_op(op, left.dtype, other.dtype, kind="binary") if op1 is not op2: - left_dtype = unify(op1.type, op2.type, is_right_scalar=True) - right_dtype = unify(op1.type2, op2.type2, is_left_scalar=True) + left_dtype = unify(op1.type, op2.type) + right_dtype = unify(op1.type2, op2.type2) op = get_typed_op(op, left_dtype, right_dtype, kind="binary") else: op = op1 diff --git a/graphblas/core/udt.py b/graphblas/core/udt.py index 0f91d7966..63e9448f5 100644 --- a/graphblas/core/udt.py +++ b/graphblas/core/udt.py @@ -31,7 +31,7 @@ from ..exceptions import DomainMismatch from . import _has_numba, ffi, utils -from .dtypes import _nats_in_leaf_units, _registry, _view_if_same_layout, lookup_dtype +from .dtypes import _iter_leaves, _nats_in_leaf_units, _registry, _view_if_same_layout, lookup_dtype from .utils import output_type # The built-in ops that lift to UDTs, each with the Python operator or function @@ -104,6 +104,26 @@ def _strip_leading_ones(shape): return shape[i:] +def _record_leaf_mismatch(leaves_x, leaves_y): + """Return where the leaves of two records do not pair, or ``None`` if they do. + + ``leaves_x`` and ``leaves_y`` are :func:`_iter_record_leaves` lists. The + operands of a lifted op (:func:`_check_udt_pair`), and an element and the + one it is stored in (:func:`_corresponding_leaves`), pair as records when + they nest the same way, with the same field names in the same order at + every level, and each field has the same shape in both; field dtypes may + differ. The result is ``"nesting"``, or ``(c_access, shape_x, shape_y)`` + for the first field whose shapes differ. Array UDTs pair by rules that + differ on purpose: operands broadcast, a stored element does not. + """ + if [py for py, _c, _d in leaves_x] != [py for py, _c, _d in leaves_y]: + return "nesting" + for (_py, c, x), (_py_y, _c_y, y) in zip(leaves_x, leaves_y, strict=True): + if x.shape != y.shape: + return c, x.shape, y.shape + return None + + # Which operands an op on UDTs takes, and the type of its result. @@ -166,7 +186,8 @@ def _check_udt_pair(op_name, dtype, dtype2, info_x, info_y): # operand is leaf ``i`` of the other. leaves_x = list(_iter_record_leaves(dtype.np_type)) leaves_y = list(_iter_record_leaves(dtype2.np_type)) - if [py for py, _c, _d in leaves_x] != [py for py, _c, _d in leaves_y]: + mismatch = _record_leaf_mismatch(leaves_x, leaves_y) + if mismatch == "nesting": raise KeyError( f"binary.{op_name} does not work with ({dtype}, {dtype2}): " f"record UDTs must nest the same way, with the same field names in the " @@ -180,13 +201,13 @@ def _check_udt_pair(op_name, dtype, dtype2, info_x, info_y): # broadcast would work one way round only, since the arithmetic ops # write into a field shaped like the left operand's: ``(3,)`` plus # ``(1,)`` works, ``(1,)`` plus ``(3,)`` does not. - for (_py, c, leaf_x), (_py_y, _c_y, leaf_y) in zip(leaves_x, leaves_y, strict=True): - if leaf_x.shape != leaf_y.shape: - raise KeyError( - f"binary.{op_name} does not work with ({dtype}, {dtype2}): " - f"record UDTs must have the same shape for each field; field {c} is " - f"{leaf_x.shape} vs {leaf_y.shape}." - ) + if mismatch is not None: + c, shape_x, shape_y = mismatch + raise KeyError( + f"binary.{op_name} does not work with ({dtype}, {dtype2}): " + f"record UDTs must have the same shape for each field; field {c} is " + f"{shape_x} vs {shape_y}." + ) return # Array UDTs broadcast as numpy arrays do, so ``(3, 1)`` with ``(1, 4)`` # gives ``(3, 4)``; the generated code reads each operand through a flat @@ -386,13 +407,7 @@ def _check_leaves_compare(op_name, dtype, dtype2): already pair (:func:`_check_udt_pair`); a side that is not a record stands in every leaf of the other. """ - - def leaves(np_type): - if np_type.names is not None: - return [leaf.base for _py, _c, leaf in _iter_record_leaves(np_type)] - return [np_type.base] - - xs, ys = leaves(dtype.np_type), leaves(dtype2.np_type) + xs, ys = list(_leaf_dtypes(dtype.np_type)), list(_leaf_dtypes(dtype2.np_type)) if len(xs) == 1: xs = xs * len(ys) if len(ys) == 1: @@ -432,12 +447,12 @@ def _corresponding_leaves(np_x, np_z): return None, [(base_x, base_z)] leaves_x = list(_iter_record_leaves(np_x)) leaves_z = list(_iter_record_leaves(np_z)) - if [py for py, _c, _d in leaves_x] != [py for py, _c, _d in leaves_z]: + mismatch = _record_leaf_mismatch(leaves_x, leaves_z) + if mismatch == "nesting": return "records must have the same field names, in the same order, at every level", None - pairs = [(x, z) for (_, _, x), (_, _, z) in zip(leaves_x, leaves_z, strict=True)] - if any(x.shape != z.shape for x, z in pairs): + if mismatch is not None: return "records must have the same shape for each field", None - return None, [(x.base, z.base) for x, z in pairs] + return None, [(x.base, z.base) for (_, _, x), (_, _, z) in zip(leaves_x, leaves_z, strict=True)] def _udt_cast_error(dtype, to_dtype): @@ -890,11 +905,7 @@ def _leaf_arrays(arr): @lru_cache def _has_half_leaf(np_type): """Whether ``np_type`` has a float16 leaf: a record field, an array's element, or itself.""" - if np_type.subdtype is not None: - return _has_half_leaf(np_type.subdtype[0]) - if np_type.names is not None: - return any(_has_half_leaf(np_type.fields[name][0]) for name in np_type.names) - return np_type.kind == "f" and np_type.itemsize == 2 + return any(leaf.base.kind == "f" and leaf.base.itemsize == 2 for leaf in _iter_leaves(np_type)) def _refuse_half(op, *dtypes): @@ -916,12 +927,11 @@ def _refuse_half(op, *dtypes): @lru_cache def _has_narrow_float_leaf(np_type): """Whether ``np_type`` has a float leaf narrower than float64, which a float may overflow.""" - if np_type.subdtype is not None: - return _has_narrow_float_leaf(np_type.subdtype[0]) - if np_type.names is not None: - return any(_has_narrow_float_leaf(np_type.fields[name][0]) for name in np_type.names) - kind, size = np_type.kind, np_type.itemsize - return (kind == "f" and size < 8) or (kind == "c" and size < 16) + for leaf in _iter_leaves(np_type): + kind, size = leaf.base.kind, leaf.base.itemsize + if (kind == "f" and size < 8) or (kind == "c" and size < 16): + return True + return False @lru_cache diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py index 17676ac51..b7eb441ee 100644 --- a/graphblas/core/vector.py +++ b/graphblas/core/vector.py @@ -1121,7 +1121,7 @@ def to_dense(self, fill_value=None, dtype=None, **opts): keyword_name="fill_value", extra_message="Literal scalars also accepted.", ) - dtype = unify(fill_value.dtype, self.dtype, is_left_scalar=True) + dtype = unify(fill_value.dtype, self.dtype) rv = self.dup(dtype, clear=True, name="to_dense", **opts) rv(**opts) << fill_value @@ -1400,8 +1400,8 @@ def _ewise_union(self, other, op, left_default, right_default, is_infix=False): op1 = get_typed_op(op, self.dtype, right.dtype, kind="binary") op2 = get_typed_op(op, left.dtype, other.dtype, kind="binary") if op1 is not op2: - left_dtype = unify(op1.type, op2.type, is_right_scalar=True) - right_dtype = unify(op1.type2, op2.type2, is_left_scalar=True) + left_dtype = unify(op1.type, op2.type) + right_dtype = unify(op1.type2, op2.type2) op = get_typed_op(op, left_dtype, right_dtype, kind="binary") else: op = op1 @@ -1611,7 +1611,7 @@ def apply(self, op, right=None, *, left=None): extra_message="Literal scalars also accepted.", op=op, ) - op = get_typed_op(op, left.dtype, self.dtype, is_left_scalar=True, kind="binary") + op = get_typed_op(op, left.dtype, self.dtype, kind="binary") if opclass == "Monoid": op = op.binaryop else: @@ -1649,12 +1649,10 @@ def apply(self, op, right=None, *, left=None): op=op, ) if opclass in {"IndexUnaryOp", "SelectOp"}: - op = get_typed_op( - op, self.dtype, right.dtype, is_right_scalar=True, kind="indexunary" - ) + op = get_typed_op(op, self.dtype, right.dtype, kind="indexunary") cfunc_method = "IndexOp" else: - op = get_typed_op(op, self.dtype, right.dtype, is_right_scalar=True, kind="binary") + op = get_typed_op(op, self.dtype, right.dtype, kind="binary") cfunc_method = "BinaryOp2nd" if opclass == "Monoid": op = op.binaryop @@ -1780,7 +1778,7 @@ def select(self, op, thunk=None): size=self._size, dtype=self.dtype, ) - op = get_typed_op(op, self.dtype, thunk.dtype, is_right_scalar=True, kind="select") + op = get_typed_op(op, self.dtype, thunk.dtype, kind="select") op, thunk = _value_select_in_own_type(op, self.dtype, thunk, given) self._expect_op(op, ("SelectOp", "IndexUnaryOp"), within=method_name, argname="op") if thunk._is_cscalar: diff --git a/graphblas/tests/test_ssjit.py b/graphblas/tests/test_ssjit.py index 8627d80e3..f4329237e 100644 --- a/graphblas/tests/test_ssjit.py +++ b/graphblas/tests/test_ssjit.py @@ -1306,6 +1306,13 @@ def test_jit_udt_np_type_numba_cannot_type(): ) assert isinstance(exc_info.value.__cause__, AttributeError) assert not hasattr(dtypes.ss, "_jit_bool_pair") + # So does a field that is an array of arrays of bool, which numpy keeps unflattened + nested = np.dtype([("d", np.dtype((np.bool_, (2,))), (3,))]) + with pytest.raises(ValueError, match="cannot type an array of bool"): + dtypes.ss.register_new( + "_jit_bool_nest", "typedef struct { uint8_t d [6] ; } _jit_bool_nest ;", np_type=nested + ) + assert not hasattr(dtypes.ss, "_jit_bool_nest") # The failure leaves the name free, so it registers with an np_type Numba can type. dtype = dtypes.ss.register_new( "_jit_bool_pair",