Summary
Shape errors construct silently instead of throwing; the malformed nodes later hit the evaluator with mismatched buffer sizes (out-of-bounds reads / garbage sums in tensor_data_add/tensor_data_inner_product, which trust the parent's dim()/rank()). Reproduced at 0f6c0b9 (construction side):
X(3D, rank2) + C(3D, rank4) accepted — node claims rank 4 with mixed-rank children (tensor_simplifier_add.h:36-41 builds tensor_add(m_lhs.dim(), m_rhs.rank()) — mixed provenance, no equality check).
X(3D) + W(2D) accepted, claiming dim 3.
inner_product(X_3D, {2}, W_2D, {1}) accepted — the dim check in the inner_product_wrapper ctor is commented out (tensor/wrappers/inner_product_wrapper.h:19-40); index bounds/pairing arity not validated either.
dot_product's precondition (src/numsim_cas/tensor_to_scalar/tensor_to_scalar_functions.cpp:20-21) uses || where a scalar result requires && — and it's a debug-only assert (NDEBUG-elided). dot(u) accepts any rank but dcontract_self_op only evaluates rank 2, so dot(u) on a vector constructs fine and fails at evaluation.
Severity: medium (needs a user shape error, but then silently wrong / UB). This is the remaining half of the closed #53, the same assert-vs-throw pattern as #295/#292.
Fix
- Tensor
add_fn/sub_fn tag_invokes: throw invalid_expression_error on dim or rank mismatch (one check each).
inner_product_wrapper ctor: restore the dim check; validate lhs_indices.size() == rhs_indices.size() and index bounds (mirror permute_indices).
dot_product: change || → && and make it a thrown invalid_expression_error (not assert); gate dot()'s self-contraction to ranks its evaluator supports, or extend the evaluator.
Tests (lock-in)
TEST(ShapeValidation, MismatchedAddThrows) {
auto [X] = make_tensor_variable(std::tuple{"X", 3, 2});
auto [C] = make_tensor_variable(std::tuple{"C", 3, 4});
auto [W] = make_tensor_variable(std::tuple{"W", 2, 2});
EXPECT_THROW(X + C, invalid_expression_error);
EXPECT_THROW(X + W, invalid_expression_error);
}
TEST(ShapeValidation, InnerProductDimAndIndices) {
auto [X] = make_tensor_variable(std::tuple{"X", 3, 2});
auto [W] = make_tensor_variable(std::tuple{"W", 2, 2});
EXPECT_THROW(inner_product(X, sequence{2}, W, sequence{1}), invalid_expression_error);
EXPECT_THROW(inner_product(X, sequence{1,2}, X, sequence{1}), invalid_expression_error); // arity
}
TEST(ShapeValidation, DotProduct) {
auto [u] = make_tensor_variable(std::tuple{"u", 3, 1});
// release-build behavior must match debug: mismatched sequence sizes throw
}
Summary
Shape errors construct silently instead of throwing; the malformed nodes later hit the evaluator with mismatched buffer sizes (out-of-bounds reads / garbage sums in
tensor_data_add/tensor_data_inner_product, which trust the parent'sdim()/rank()). Reproduced at 0f6c0b9 (construction side):X(3D, rank2) + C(3D, rank4)accepted — node claims rank 4 with mixed-rank children (tensor_simplifier_add.h:36-41buildstensor_add(m_lhs.dim(), m_rhs.rank())— mixed provenance, no equality check).X(3D) + W(2D)accepted, claiming dim 3.inner_product(X_3D, {2}, W_2D, {1})accepted — the dim check in theinner_product_wrapperctor is commented out (tensor/wrappers/inner_product_wrapper.h:19-40); index bounds/pairing arity not validated either.dot_product's precondition (src/numsim_cas/tensor_to_scalar/tensor_to_scalar_functions.cpp:20-21) uses||where a scalar result requires&&— and it's a debug-only assert (NDEBUG-elided).dot(u)accepts any rank butdcontract_self_oponly evaluates rank 2, sodot(u)on a vector constructs fine and fails at evaluation.Severity: medium (needs a user shape error, but then silently wrong / UB). This is the remaining half of the closed #53, the same assert-vs-throw pattern as #295/#292.
Fix
add_fn/sub_fntag_invokes: throwinvalid_expression_errorondimorrankmismatch (one check each).inner_product_wrapperctor: restore the dim check; validatelhs_indices.size() == rhs_indices.size()and index bounds (mirrorpermute_indices).dot_product: change||→&&and make it a throwninvalid_expression_error(not assert); gatedot()'s self-contraction to ranks its evaluator supports, or extend the evaluator.Tests (lock-in)