refactor(linalg): constrain free Tensor/UniTensor scalar operators to scalar-like types (#1003)#1093
refactor(linalg): constrain free Tensor/UniTensor scalar operators to scalar-like types (#1003)#1093yingjerkao wants to merge 2 commits into
Conversation
… scalar-like types (#1003) Ian's item 3 from the #1003 review. The free namespace-scope arithmetic and comparison operators for Tensor and UniTensor (`+ - * / % ==` paired with a scalar) were declared as unconstrained `template <class T>`, so an arbitrary std / user-defined type could be deduced as T and made the operator a viable candidate in overload resolution (e.g. under `using namespace cytnx`), even though the operators are only ever instantiated/specialized for the cytnx scalar surface. Introduce a `cytnx_scalar_like` concept in linalg.hpp that admits exactly that surface -- the cytnx dtype scalars (reusing the existing `CytnxType` concept), `cytnx::Scalar`, and the `Tensor::Tproxy` / `Scalar::Sproxy` element proxies -- and constrain the 22 free operator primary declarations with it. The Tensor operators are full specializations (constraining the primary declaration is enough); the UniTensor operators are generic templates, so their 8 generic definitions in Add/Sub/Mul/Div/Mod.cpp get the matching constraint. `linalg::Add/Sub/Mul/Div/Mod/Cpr` (explicitly named, not operator-resolution pollution) and the matrix-exponential `ExpH/ExpM` templates are intentionally left unconstrained. This does not break valid use: every builtin scalar literal deduces to a cytnx alias (int -> cytnx_int32, etc.); a non-scalar operand that was accepted before only ever failed at link (no instantiation) and now fails with a clear overload error. Adds tests/operator_constraint_test.cpp: compile-time guards that `cytnx_scalar_like` admits the scalars/proxies and rejects other types, that `scalar-on-the-left <op> Tensor/UniTensor` is viable for scalars and not for a non-scalar struct (the guards fail on pre-fix code), plus a runtime check that scalar operators and Tensor<op>Tensor still work. Testing (CPU, openblas): library + test_main build clean; OperatorConstraint passes; pybind tensor_py.cpp / unitensor_py.cpp compile clean (they use the member .Add()/linalg::Add paths, not the free operators). clang-format-14 clean. Advances #1003 (Ian's operator-hygiene fold-in, item 3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a C++20 concept cytnx_scalar_like to constrain the free arithmetic and comparison operators (+ - * / % ==) for Tensor and UniTensor to cytnx dtype scalars, cytnx::Scalar, and element proxies. This prevents arbitrary standard or user-defined types from being implicitly converted and used as operands. Corresponding unit tests have been added to verify these constraints at compile-time and runtime. The reviewer suggested explicitly including <type_traits> in include/linalg.hpp to ensure compilation robustness and avoid relying on transitive includes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| namespace cytnx { |
There was a problem hiding this comment.
The new cytnx_scalar_like concept relies on std::remove_cvref_t and std::is_same_v, which are defined in <type_traits>. To ensure compilation robustness and avoid relying on transitive includes, please explicitly include <type_traits>.
| namespace cytnx { | |
| #include <type_traits>\n\nnamespace cytnx { |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1489ecd8a9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| template <cytnx_scalar_like T> | ||
| Tensor operator+(const Tensor &Lt, const T &rc); |
There was a problem hiding this comment.
Constrain the Tensor forward declarations too
When a user writes tensor + some_non_scalar (and similarly -, *, or / with the scalar-looking operand on the right), this constrained declaration does not actually remove the unconstrained candidate: include/Tensor.hpp is included before linalg.hpp and still forward-declares template <class T> Tensor operator+(const Tensor&, const T&). In C++ that leaves a separate unconstrained overload viable whenever T fails cytnx_scalar_like, so those calls still compile until the same undefined-reference failure this change was meant to eliminate; the new tests only cover the scalar-on-the-left direction.
Useful? React with 👍 / 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1093 +/- ##
===========================================
+ Coverage 57.98% 72.71% +14.73%
===========================================
Files 229 226 -3
Lines 33459 28151 -5308
Branches 71 71
===========================================
+ Hits 19401 20471 +1070
+ Misses 14036 7659 -6377
+ Partials 22 21 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 48 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
Review follow-up (#1093): the cytnx_scalar_like concept uses std::remove_cvref_t and std::is_same_v, so include <type_traits> directly rather than relying on a transitive include (Gemini). Also documents in operator_constraint_test.cpp the scalar-on-the-right gap Codex raised: `Tensor + non_scalar` still binds the unconstrained free operator forward- declared in Tensor.hpp. Those forward declarations are load-bearing for Tensor's inline member templates (e.g. `Mul(const T&){ return *this * rhs; }`, and operator* has no member form), and cannot be constrained in place because cytnx_scalar_like depends on the not-yet-complete Tensor::Tproxy at that point; closing the gap needs a concept/header reorganization, deferred to a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bff592b25e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| std::is_same_v<std::remove_cvref_t<T>, Tensor::Tproxy> || | ||
| std::is_same_v<std::remove_cvref_t<T>, Scalar::Sproxy>; |
There was a problem hiding this comment.
Exclude proxies from unsupported operators
Because this one concept is used for all of the constrained % and == declarations too, Tensor::Tproxy and Scalar::Sproxy remain viable operands even where no specialization/definition exists; repo-wide search shows no operator%/operator== proxy specializations, and no Tensor::Tproxy specializations for the UniTensor operators. In contexts such as tensor.at({0}) == tensor or tensor.at({0}) % tensor, overload resolution now accepts the operand as cytnx_scalar_like but the program still fails with an undefined reference, which preserves the link-time failure this change is meant to turn into a non-viable overload.
Useful? React with 👍 / 👎.
|
Thanks — addressed what I safely could:
|
|
Review status (bot threads) — all three look already addressed by the current branch head:
Ready to resolve. |
|
To use Codex here, create an environment for this repo. |
ianmccul
left a comment
There was a problem hiding this comment.
-
Tensor RHS remains unconstrained.
Lines 30 to 37 in bff592b
-
cytnx_scalar_like does not describe the implemented concept.
Tensor::Tproxyis a deferredTensorselection, potentially of any rank, not inherently scalar. Some proxy arithmetic works by materializing it as aTensor:
Tensor operator+<Tensor::Tproxy>(...) {
return Tensor(lc) + Rt;
}That should be a proxy-specific overload, not membership in a scalar concept.
Worse, the concept admits combinations with no implementation:
Tproxy % Tensor
Tproxy == Tensor
Tproxy + UniTensor
Scalar % UniTensor
Sproxy % UniTensorThese declarations participate in overload resolution but end in undefined references.
- Implicit Tensor construction still admits non-scalars.
The test itself avoids containers because std::vector<cytnx_uint64> converts implicitly toTensor.Therefore:
std::vector<cytnx_uint64> shape{2, 3};
auto result = shape + tensor;can bind Tensor + Tensor. That directly contradicts the PR’s claim that arbitrary standard-library operands become non-viable. The Tensor shape constructors should ultimately be explicit.
- The tests encode the known hole.
They test only selected scalar-left expressions and deliberately leave scalar-right untested. They should cover every affected operator, both directions, and all admitted proxy categories.
Problem
Ian's item 3 from the #1003 review. The free namespace-scope arithmetic/comparison operators for
TensorandUniTensor(+ - * / % ==paired with a scalar) were declared as unconstrainedtemplate <class T>:So an arbitrary
std/ user-defined type could be deduced asTand made the operator a viable candidate in overload resolution (e.g. underusing namespace cytnx), even though the operators are only ever specialized/instantiated for the cytnx scalar surface.Fix
Add a
cytnx_scalar_likeconcept inlinalg.hppadmitting exactly that surface — the cytnx dtype scalars (reusing the existingCytnxTypeconcept),cytnx::Scalar, and theTensor::Tproxy/Scalar::Sproxyelement proxies — and constrain the 22 free operator primary declarations with it.Tensoroperators are full specializations, so constraining the primary declaration suffices (no.cppchange).UniTensoroperators are generic templates, so their 8 generic definitions inAdd/Sub/Mul/Div/Mod.cppget the matching constraint (a constrained declaration needs a constrained definition).linalg::Add/Sub/Mul/Div/Mod/Cpr(explicitly named — not an overload-resolution pollution vector; pybind uses these with explicit scalar casts) and theExpH/ExpMtemplates are intentionally left unconstrained.Not a breaking change for valid use: every builtin scalar literal deduces to a cytnx alias (
int→cytnx_int32,1.0→cytnx_double, …); a non-scalar operand that "compiled" before only ever failed at link (no instantiation exists) and now fails with a clear overload error instead.Adds
tests/operator_constraint_test.cpp: compile-time guards thatcytnx_scalar_likeadmits the scalars/proxies and rejects other types, thatscalar-on-the-left <op> Tensor/UniTensoris viable for scalars but not for a non-scalar struct (these fail on pre-fix code), plus a runtime check that scalar operators andTensor<op>Tensorstill work.Testing
CPU (openblas): library +
test_mainbuild clean (the full library build confirmed no other call site relied on the loose operators);OperatorConstraintpasses;pybind/tensor_py.cppandunitensor_py.cppcompile clean. clang-format-14 clean.Advances #1003 (Ian's operator-hygiene fold-in, item 3). Independent of the sibling #1003 branches (#1091 GPU in-place, #1092 complex_arithmetic).
🤖 Generated with Claude Code