From 587692a505ae6a756b5bcc2d9165d557022260c3 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 3 Aug 2026 15:42:48 -0500 Subject: [PATCH] Extend the wrong-kind mask guard to dup and whole-object update A.dup(mask=v.S) and C(mask=v.S) << A assign with C(mask)[...] = A, which builds a __setitem__ expression, so the strict mask-kind check in BaseType._update did not apply and a Vector mask on a Matrix output reached GrB_Matrix_assign, leaking a raw cffi error: TypeError: Error calling GrB_Matrix_assign: ... initializer for ctype 'struct GB_Matrix_opaque *' must be a pointer to same type, not cdata 'struct GB_Vector_opaque *' Validate the mask kind in the Matrix-valued branch of Matrix._prep_for_assign, mirroring the guard the Vector-valued branch already has. This also covers C(v.S)[:, :] << A and the submask form C[:, :](v.S) << A. Row and column assignment with a Vector mask (C(v.S)[0, :] << w and friends) is handled in earlier branches and remains valid. The guard keys on mask.parent.ndim rather than exact type, the same idiom _check_mask uses, so a Vector subclass's mask is caught too. Two pre-existing guards in the Vector-valued branch and one more in the scalar path still use exact-type checks and carry the same subclass hole; they predate this commit and are left for their own change. --- graphblas/core/matrix.py | 7 +++++ graphblas/tests/test_matrix.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 3fd3e6557..526833eda 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -3416,6 +3416,13 @@ def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, o within=method_name, extra_message=extra_message, ) + if mask is not None and mask.parent.ndim != 2: + # Matrix value, Vector mask, Matrix index + # C(m)[I, J] << A + # C[I, J](m) << A + # This also catches whole-object updates such as `C(m) << A` + # and `C.dup(mask=m)`, which assign with `C(m)[...] = A`. + raise TypeError("Unable to use Vector mask on Matrix assignment to a Matrix") if is_submask: # C[I, J](M) << A expr_repr = ( diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 5d23d853b..3176a310e 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -4618,6 +4618,61 @@ def test_wrong_kind_mask_on_matrix_raises(): assert A[0, [0, 1, 2]].new(input_mask=m.S) is not None +def test_wrong_kind_mask_on_dup_and_full_assign(): + # dup and whole-object update assign with `C(mask)[...] = A`, so a + # Vector mask used to reach GrB_Matrix_assign and leak the same raw + # cffi error as full-matrix operations. + A = Matrix(int, 3, 3) + A[0, 0] = 1 + v = Vector(bool, 3) + v[0] = True + v[1] = True + err = "Unable to use Vector mask on Matrix assignment to a Matrix" + with pytest.raises(TypeError, match=err): + A.dup(mask=v.S) + C = Matrix(int, 3, 3) + with pytest.raises(TypeError, match=err): + C(v.S) << A + with pytest.raises(TypeError, match=err): + C(v.V) << A + with pytest.raises(TypeError, match=err): + C(~v.S) << A + with pytest.raises(TypeError, match=err): + C(v.S)[:, :] << A + with pytest.raises(TypeError, match=err): + C[:, :](v.S) << A + # A Matrix mask on these paths still works + assert A.dup(mask=A.S).isequal(A) + C(A.S) << A + assert C.isequal(A) + # A Vector mask on a Matrix row/column assignment is still valid + w = Vector(int, 3) + w[0] = 10 + w[2] = 30 + C = Matrix(int, 3, 3) + C(v.S)[0, :] << w + assert C.isequal(Matrix.from_coo([0], [0], [10], nrows=3, ncols=3)) + C.clear() + C(v.S)[:, 0] << w + assert C.isequal(Matrix.from_coo([0], [0], [10], nrows=3, ncols=3)) + C.clear() + C[0, :](v.S) << w + assert C.isequal(Matrix.from_coo([0], [0], [10], nrows=3, ncols=3)) + C.clear() + C[:, 0](v.S) << w + assert C.isequal(Matrix.from_coo([0], [0], [10], nrows=3, ncols=3)) + + # The guard keys on dimension, not exact type, so a Vector subclass's + # mask is caught too instead of leaking the raw cffi error + class _VecSub(Vector): + pass + + mv = _VecSub(bool, 3) + mv[0] = True + with pytest.raises(TypeError, match="Unable to use Vector mask"): + C(mv.S) << A + + def test_new_constructor_misuse_hint(): # `.new()` resolves expressions; it is not a constructor or an instance # method on concrete objects. Both misuses should hint the right API.