Skip to content

Rank gate on tensor_inv wrapper ctor (#292) - #294

Merged
petlenz merged 7 commits into
mainfrom
292-tensor-inv-wrapper-rank-gate
Jun 11, 2026
Merged

Rank gate on tensor_inv wrapper ctor (#292)#294
petlenz merged 7 commits into
mainfrom
292-tensor-inv-wrapper-rank-gate

Conversation

@petlenz

@petlenz petlenz commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary

Closes #292. Surfaced by code review of #291.

The inv() factory in tensor_functions.h:467 already rejects rank ≠ 2 and ≠ 4 at construction with invalid_expression_error. The tensor_inv wrapper constructor itself silently accepted any rank — so direct make_expression<tensor_inv>(rank3_T) constructions that bypass the factory produced a "valid-looking" node that misbehaved downstream (space-propagation block silently emitted nothing; every visitor needed its own belt-and-braces rank check).

Now mirrored in the wrapper ctor with the same convention.

Cascade (after 5 review passes)

  • The wrapper gate makes the belt-and-braces rank check in tensor_differentiation_wrt_scalar.cpp unreachable, so it was deleted (not kept — pass-1 review flagged "dead code + zero coverage = worse than no defense"). The surviving if (r == 2) / else if (r == 4) / else std::unreachable() shape pins the rank ∈ {2, 4} invariant at the only enforcement point.
  • The wrapper's space-propagation block was tightened from if/else if (with a stale "shouldn't be reached" trailing comment) to exhaustive if/else (pass-3 cleanup).
  • TensorDiffWrtScalarTest.InvRank3ThrowsNotImplemented (from Rank-4 tensor_inv in scalar-arg diff visitor (#287) #291) renamed to InvRank3RejectedAtConstruction — the make_expression<tensor_inv> call now throws before diff runs.
  • Pass-4 added InvWrapperCtorRank0Rejected to close the low-end test matrix.
  • Pass-5 NIT (clang-format <utility> include placement) + stale doc-comment refresh.

Test plan

  • TensorExpressionTest.InvWrapperCtor{Rank0, Rank1, Rank3, Rank5}Rejected — wrapper-level lock-ins on both sides of the {2, 4} band.
  • TensorDiffWrtScalarTest.InvRank3RejectedAtConstruction — pins the construction-time throw.
  • Existing factory-level InvRank3SymbolStillThrows untouched (verifies the factory's independent gate).
  • Full suite: 1340/1340 passing (+12 from the 4 typed-tests × 3 dims of the wrapper lock-ins).

Follow-ups (not blocking)

  • m_rank on tensor_expression is protected std::size_t (not const) — immutability is by convention, not language-enforced. A future hardening could mark it const or add a // no setter exists comment.
  • Pass-5 reviewer noted a pre-existing arg-eval-order pattern in tensor_inv.h:17 (and across tensor_if_then_else_*.h) where then_branch.get().dim() is read in the same mem-init list as std::forward<Expr>(_expr). Re-analyzed as safe (the std::forward cast doesn't move; the actual move happens deep inside ternary_op's base initialization), but worth a follow-up explanatory comment to head off future confusion.

petlenz added 7 commits June 10, 2026 20:13
The inv() factory at tensor_functions.h:467 rejects rank != 2 and != 4
at construction with invalid_expression_error. The tensor_inv wrapper
constructor itself was silently accepting any rank — so direct
`make_expression<tensor_inv>(rank3_T)` constructions that bypass the
factory produced a "valid-looking" rank-3 node that misbehaved
downstream:

  - The space-propagation block branches on r == 2 / r == 4 and silently
    falls through for other ranks, leaving the result without an
    annotation.
  - Every consumer (evaluator, diff visitors, printer) had to grow its
    own belt-and-braces rank check, or just produce wrong results.

Mirror the factory's rank gate in the wrapper ctor. Throws match the
factory's invalid_expression_error convention and — unlike assert —
fire in Release builds too.

Side effects:
- The belt-and-braces throw at tensor_differentiation_wrt_scalar.cpp:268
  is now structurally unreachable. Kept as defense-in-depth with a
  comment explaining the redundancy.
- TensorDiffWrtScalarTest.InvRank3ThrowsNotImplemented (from #291) is
  no longer reachable as written — the make_expression call throws
  before diff runs. Renamed to InvRank3RejectedAtConstruction and now
  asserts the construction-time throw directly.

2 new wrapper-level lock-ins in TensorExpressionTest.h:
- InvWrapperCtorRank3Rejected
- InvWrapperCtorRank5Rejected

Surfaced by the code review of #291. Closes #292.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
Review's MEDIUM: the belt-and-braces rank check at
tensor_differentiation_wrt_scalar.cpp is now structurally unreachable
after the wrapper ctor gate. Keeping it as "defense in depth" gives a
false sense of safety — there's no test that would fire if the
wrapper's gate were relaxed in the future, because the test path
catches the wrapper throw first.

Delete the dead branch. Replace with a one-line comment pinning the
rank ∈ {2, 4} contract on #292's wrapper ctor. Smaller surface, no
dead code, contract documented at the only enforcement point.

The parallel "rank-4 not implemented" throw in the *tensor-arg*
visitor (tensor_differentiation.cpp:347) is NOT dead — rank-4 inv-diff
w.r.t. a tensor remains deferred — and is left alone.

Full suite: 1334/1334 passing.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
Address three findings from the pass-2 critical review:

- [HIGH] CHANGELOG entry contradicted the pass-1 commit. The entry said
  the belt-and-braces throw was "kept as defense-in-depth" but pass-1
  deleted it. Fix the wording to match reality and explain why the
  surviving `if/else if/std::unreachable()` shape is the right
  documentation of the rank ∈ {2,4} contract.

- [MEDIUM] The visitor's `else` branch silently applied the rank-4
  contraction layout. If a future wrapper-gate relaxation admitted
  rank-6, the `else` would emit a malformed AST instead of failing.
  Replace `if (r == 2) ... else { rank-4 }` with `if (r == 2) ...
  else if (r == 4) ... else std::unreachable()`. Same `std::unreachable`
  pattern as projector_algebra.h:48. Caught in Debug as UB hint, abort
  in sanitizer builds, vs silently-wrong in Release with the prior
  shape.

- [MEDIUM] Wrapper-gate had rank-3 / rank-5 lock-ins but no low-end
  coverage. tensor_expression accepts any rank std::size_t (no rank >= 2
  invariant on the type itself), so rank-1 is technically constructible.
  Add InvWrapperCtorRank1Rejected.

Full suite: 1337/1337 passing (+3 from rank-1 lock-in plus the
unreachable-branch addition).

The pass-2 NIT about the wrapper's space-propagation comment
("inv() factory rejects them") is left for a follow-up — fixing the
comment also wants the if/else if → if/else cleanup the original
review flagged as a NIT. Bundling those separately keeps each
commit's intent crisp.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
With the rank gate enforcing rank ∈ {2, 4}, the trailing "Other ranks:
inv() factory rejects them; this branch shouldn't be reached" comment
on the if/else if chain was stale — the wrapper rejects them, not the
factory, and the trailing fall-through is dead. Tighten to an
exhaustive if/else with a header comment crediting the gate.

Pure refactor — no behavior change. Full suite: 1337/1337 passing.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
Pass-4 review LOW finding: rank-0 (tensor_expression accepts any size_t
rank, including 0 — scalar-as-tensor degenerate form) was not in the
wrapper-gate test matrix. Existing rank-1, rank-3, rank-5 lock-ins all
exercise the same gate; rank-0 closes the low-end of the integer
range.

The PR body (also updated via gh api PATCH) now correctly reflects the
pass-1 deletion of the visitor's belt-and-braces rank check; the prior
body still claimed it was kept as defense-in-depth.

Full suite still 1337+1 → 1338 tests passing (typed-test × 3 dims =
3 instances of the new rank-0 lock-in).

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
CI clang-format-18 flagged the <utility> include placement (line 13
between project includes) as the pass-3 reviewer's NIT predicted.
Move it to the correct sort position.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
Pass-5 review flagged the doc-comment above the tensor_inv operator in
tensor_differentiation_wrt_scalar.cpp as stale: it still described the
belt-and-braces throw that pass-1 (commit d2ed390) deleted. After 4
prior review passes touching this file's code, the comment block was
never refreshed.

New text describes the actual contract: rank ∈ {2, 4} pinned by the
wrapper ctor (#292); the body's std::unreachable() catches drift if a
future wrapper relaxation admits a higher rank.

PR body also updated via gh api PATCH to fix the test count
(1337 → 1340, since the wrapper-ctor lock-ins are typed-tests × 3 dims)
and remove the now-stale 'rank-0 not in matrix' line from the
Follow-ups section.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
@petlenz
petlenz merged commit 96c7055 into main Jun 11, 2026
23 checks passed
@petlenz
petlenz deleted the 292-tensor-inv-wrapper-rank-gate branch June 11, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tensor_inv wrapper ctor lacks rank gate (rank-3 silently constructible)

1 participant