Skip to content

refactor(Symmetry): make Symmetry a plain value type over std::variant#1010

Merged
pcchen merged 3 commits into
masterfrom
refactor/symmetry-value-type
Jul 9, 2026
Merged

refactor(Symmetry): make Symmetry a plain value type over std::variant#1010
pcchen merged 3 commits into
masterfrom
refactor/symmetry-value-type

Conversation

@yingjerkao

@yingjerkao yingjerkao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

Fixes #842. Symmetry was a boost::intrusive_ptr<Symmetry_base> PIMPL over four virtual subclasses — reference semantics, virtual dispatch, and clone() boilerplate for what is a small immutable value.

What changed

Symmetry now holds a by-value std::variant<U1Symmetry, ZnSymmetry, FermionParitySymmetry, FermionNumberSymmetry>; every method dispatches via std::visit. The generic visit lambdas are the completeness contract: adding a fifth kind fails compilation until all rule methods exist.

Preserved exactly: the full public API (verified signature-by-signature — factories, combine_rule(_) including out-param forms, stype()/stype_str(), the legacy int& n() const accessor now backed by a mutable member, comparison = stype+n, all error messages) and the Save/Load byte format — pinned by binary fixtures generated at the base commit and byte-compared in tests, both directions, including a mixed-symmetry Bond. (One deliberate exception, per review: the non-mutating rule methods — check_qnum, check_qnums, the vector combine_rule, both combine_rule_ overloads, reverse_rule_ — are now const-qualified. This only relaxes callability; no signature is otherwise changed.)

Deliberate notes: is(Symmetry, Symmetry) now means address identity (copies are no longer is their source); likewise, writing through the legacy int &n() const reference no longer propagates to copies (each Symmetry owns its value) — the honest semantics for a value type; the sole in-tree observer (a doc example) is updated. Of the two pre-existing bugs found during the work, FermionParitySymmetry::check_qnums (always false for non-empty input) has since been fixed on master by #1012 and this branch ports that fix into the variant implementation; the NEED_GEN_COMMON_DATA=1 exit crash remains preserved and tracked separately.

Testing

20 new gtests (byte-compat fixtures, API pinning, quirk pin, bad-magic Load rejection, signature static_assert). Full suite: 1106 passed / 11 skipped / only the 4 pre-existing failures addressed by #977. pytest 57 passed; the pybind surface diff is empty.

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Symmetry class from an intrusive pointer-based implementation (boost::intrusive_ptr) to a plain value type using std::variant and std::visit. This change makes Symmetry objects independent copies upon assignment and copy construction. The PR also adds comprehensive unit tests in tests/Symmetry_test.cpp to ensure byte-compatibility and API correctness. The review feedback suggests improving const-correctness across several member functions in Symmetry (such as check_qnum, check_qnums, combine_rule, combine_rule_, and reverse_rule_) by marking them as const.

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.

Comment thread include/Symmetry.hpp Outdated
Comment on lines +362 to +364
bool check_qnum(const cytnx_int64 &qnum) {
return std::visit([&](const auto &kind) { return kind.check_qnum(qnum); }, this->_impl);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check_qnum method does not modify the Symmetry object and should be marked const. This ensures const-correctness and allows the method to be called on const Symmetry instances, which is a common use case.

Suggested change
bool check_qnum(const cytnx_int64 &qnum) {
return std::visit([&](const auto &kind) { return kind.check_qnum(qnum); }, this->_impl);
}
bool check_qnum(const cytnx_int64 &qnum) const {
return std::visit([&](const auto &kind) { return kind.check_qnum(qnum); }, this->_impl);
}

Comment thread include/Symmetry.hpp Outdated
Comment on lines 373 to 375
bool check_qnums(const std::vector<cytnx_int64> &qnums) {
return this->_impl->check_qnums(qnums);
return std::visit([&](const auto &kind) { return kind.check_qnums(qnums); }, this->_impl);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check_qnums method does not modify the Symmetry object and should be marked const to ensure const-correctness and allow usage on const Symmetry instances.

Suggested change
bool check_qnums(const std::vector<cytnx_int64> &qnums) {
return this->_impl->check_qnums(qnums);
return std::visit([&](const auto &kind) { return kind.check_qnums(qnums); }, this->_impl);
}
bool check_qnums(const std::vector<cytnx_int64> &qnums) const {
return std::visit([&](const auto &kind) { return kind.check_qnums(qnums); }, this->_impl);
}

Comment thread include/Symmetry.hpp
Comment on lines 383 to 388
std::vector<cytnx_int64> combine_rule(const std::vector<cytnx_int64> &inL,
const std::vector<cytnx_int64> &inR) {
return this->_impl->combine_rule(inL, inR);
std::vector<cytnx_int64> out;
this->combine_rule_(out, inL, inR);
return out;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The vector overload of combine_rule does not modify the Symmetry object and should be marked const to match the scalar overload and ensure const-correctness.

Suggested change
std::vector<cytnx_int64> combine_rule(const std::vector<cytnx_int64> &inL,
const std::vector<cytnx_int64> &inR) {
return this->_impl->combine_rule(inL, inR);
std::vector<cytnx_int64> out;
this->combine_rule_(out, inL, inR);
return out;
}
std::vector<cytnx_int64> combine_rule(const std::vector<cytnx_int64> &inL,
const std::vector<cytnx_int64> &inR) const {
std::vector<cytnx_int64> out;
this->combine_rule_(out, inL, inR);
return out;
}

Comment thread include/Symmetry.hpp
Comment on lines 397 to 400
void combine_rule_(std::vector<cytnx_int64> &out, const std::vector<cytnx_int64> &inL,
const std::vector<cytnx_int64> &inR) {
this->_impl->combine_rule_(out, inL, inR);
std::visit([&](const auto &kind) { kind.combine_rule_(out, inL, inR); }, this->_impl);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The vector overload of combine_rule_ does not modify the Symmetry object and should be marked const to ensure const-correctness.

Suggested change
void combine_rule_(std::vector<cytnx_int64> &out, const std::vector<cytnx_int64> &inL,
const std::vector<cytnx_int64> &inR) {
this->_impl->combine_rule_(out, inL, inR);
std::visit([&](const auto &kind) { kind.combine_rule_(out, inL, inR); }, this->_impl);
}
void combine_rule_(std::vector<cytnx_int64> &out, const std::vector<cytnx_int64> &inL,
const std::vector<cytnx_int64> &inR) const {
std::visit([&](const auto &kind) { kind.combine_rule_(out, inL, inR); }, this->_impl);
}

Comment thread include/Symmetry.hpp
Comment on lines 423 to 427
void combine_rule_(cytnx_int64 &out, const cytnx_int64 &inL, const cytnx_int64 &inR,
const bool &is_reverse = false) {
this->_impl->combine_rule_(out, inL, inR, is_reverse);
std::visit([&](const auto &kind) { kind.combine_rule_(out, inL, inR, is_reverse); },
this->_impl);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The scalar overload of combine_rule_ does not modify the Symmetry object and should be marked const to ensure const-correctness.

Suggested change
void combine_rule_(cytnx_int64 &out, const cytnx_int64 &inL, const cytnx_int64 &inR,
const bool &is_reverse = false) {
this->_impl->combine_rule_(out, inL, inR, is_reverse);
std::visit([&](const auto &kind) { kind.combine_rule_(out, inL, inR, is_reverse); },
this->_impl);
}
void combine_rule_(cytnx_int64 &out, const cytnx_int64 &inL, const cytnx_int64 &inR,
const bool &is_reverse = false) const {
std::visit([&](const auto &kind) { kind.combine_rule_(out, inL, inR, is_reverse); },
this->_impl);
}

Comment thread include/Symmetry.hpp Outdated
Comment on lines 437 to 439
void reverse_rule_(cytnx_int64 &out, const cytnx_int64 &in) {
this->_impl->reverse_rule_(out, in);
std::visit([&](const auto &kind) { kind.reverse_rule_(out, in); }, this->_impl);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The reverse_rule_ method does not modify the Symmetry object and should be marked const to ensure const-correctness.

Suggested change
void reverse_rule_(cytnx_int64 &out, const cytnx_int64 &in) {
this->_impl->reverse_rule_(out, in);
std::visit([&](const auto &kind) { kind.reverse_rule_(out, in); }, this->_impl);
}
void reverse_rule_(cytnx_int64 &out, const cytnx_int64 &in) const {
std::visit([&](const auto &kind) { kind.reverse_rule_(out, in); }, this->_impl);
}

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.11765% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 33.15%. Comparing base (8f69f05) to head (15b2d99).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
include/Symmetry.hpp 58.69% 2 Missing and 17 partials ⚠️
src/Symmetry.cpp 95.23% 0 Missing and 1 partial ⚠️
src/utils/is.cpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1010      +/-   ##
==========================================
+ Coverage   32.99%   33.15%   +0.15%     
==========================================
  Files         230      230              
  Lines       33076    33031      -45     
  Branches    13787    13794       +7     
==========================================
+ Hits        10915    10950      +35     
+ Misses      14804    14702     -102     
- Partials     7357     7379      +22     
Flag Coverage Δ
cpp 32.79% <69.11%> (+0.15%) ⬆️
python 61.84% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 33.94% <69.11%> (+0.18%) ⬆️
Python bindings 25.12% <ø> (-0.03%) ⬇️
Python package 61.84% <ø> (ø)
Files with missing lines Coverage Δ
src/Symmetry.cpp 64.28% <95.23%> (+29.99%) ⬆️
src/utils/is.cpp 20.00% <0.00%> (ø)
include/Symmetry.hpp 63.38% <58.69%> (+6.53%) ⬆️

... and 2 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8f69f05...15b2d99. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pcchen

pcchen commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Converts Symmetry from a boost::intrusive_ptr<Symmetry_base> PIMPL (four virtual subclasses + clone() boilerplate) to a by-value std::variant<U1/Zn/FermionParity/FermionNumber> with std::visit dispatch. A clean, well-motivated modernization of a small immutable value.

Correctness — verified

  • Variant + std::visit design is clean. Every method dispatches via a generic std::visit([](const auto& kind){...}, _impl); the generic lambdas are a real completeness contract — adding a 5th alternative fails to compile until it implements every rule method. ✅
  • Legacy n values preserved with correct ctors: U1Symmetry() : n(1), ZnSymmetry(n) (validates n>1), fPar = -2, fNum = -1 — matching the old impl (documented in-code). The Symmetry(stype,n) ctor discards the n arg for U1, but that's fine since U1Symmetry() sets n(1). ✅
  • ==/!= = stype()+n() — matches old semantics and correctly distinguishes Zn(3) vs Zn(5) by n (their stype() is both Z). ✅
  • Save/Load byte format writes/reads 777 magic (uint) + stype_id (int) + n (int) — crucially it writes stype() (the SymmetryType value), not the variant index, so old files stay loadable and new saves are byte-identical. _Load validates the 777 magic and routes through Init, which errors on an invalid stype — so a corrupt file errors rather than silently mis-loading. ✅
  • clone() const { return *this; } — correct for a value type. ✅
  • CI: BuildAndTest (C++ gtests) is actually running on this commit, and the test methodology is strong — base-generated .cysym/.cybd byte fixtures compared both directions (incl. a mixed-symmetry Bond), API static_asserts, quirk pins, and bad-magic rejection.

Findings (low / notes — none blocking)

1. A second reference->value semantic change, only partly documented. The PR flags that is() now means address identity (copies aren't is their source). The same shift applies to n(): since int& n() const returns a reference into a now-per-instance mutable member, sym.n() = k no longer propagates to copies (the old shared-impl did). Arguably safer, but it's a second observable behavior change from the reference->value move — worth adding to the "deliberate notes" alongside is().

2. int& n() const backed by mutable int n is a preserved legacy wart — a const accessor handing out a writable reference (const-mutation + thread-safety hazard). Fine for the exact-API-preservation goal and clearly documented; a good follow-up would be to make n() return int by value, since the mutable-ref-from-const pattern has no legitimate caller.

3. Stale comment // default is U1Symmetry. Both old and new actually throw on default construction (Symmetry() -> Init(-1) -> [ERROR] invalid symmetry type). Preserved behavior (verified against master), and no in-tree code default-constructs Symmetry, so it's harmless — but the comment is misleading and was carried over verbatim; worth correcting while here.

4. Preserved pre-existing bugs (FermionParitySymmetry::check_qnums always false; NEED_GEN_COMMON_DATA=1 exit crash) — honestly disclosed, pinned bit-for-bit by tests, and tracked separately. Correct call not to fix them in a "preserve exactly" refactor.

Verdict

High-quality, faithful refactor: the variant model is cleaner and cheaper than the virtual PIMPL, the public API and Save/Load byte format are preserved (structurally verified here; pinned by base-generated fixtures in the suite), and the semantic changes are deliberate and mostly documented. Findings are notes/nits, not defects. Approve-with-nits once BuildAndTest goes green (it's running now).

Posted by Claude Code on behalf of @pcchen

@pcchen

pcchen commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Correction + verification follow-up to my review above

Retracting finding #3 — it was my error. I claimed Symmetry() throws and the // default is U1Symmetry comment is stale. That's wrong: enum SymmetryType : int { Void=-99, U=-1, Z=0, fPar=-2, fNum=-3 }, so SymmetryType::U == -1 — which is exactly the constructor's default stype = -1. Symmetry() -> Init(-1) -> U1Symmetry(), i.e. it correctly default-constructs to U1, and the comment is accurate. I misread -1 as an invalid stype. Apologies — please disregard finding #3.

Byte-compat and API preservation now positively verified (I did a deeper pass on the fixtures):

  • Save/Load is genuinely byte-preserving, pinned against real old-format bytes. The .cysym fixtures decode exactly to [777][stype][n] (e.g. sym_fPar = 777,-2,-2; sym_fNum = 777,-3,-1), the four Symmetry blocks are embedded byte-identically inside bond_mixed_syms.cybd, and the generator's cited base commit d6dcd160 is confirmed the last intrusive-ptr commit and an ancestor of master. Tests load the old files and re-save freshly-built objects and EXPECT_EQ the bytes — a true old-vs-new pin, plus bad-magic rejection. The variant index is never serialized; only stype() is.
  • Public API is signature-for-signature identical (factories, combine_rule/combine_rule_ incl. out-param forms, reverse_rule(_), stype/stype_str/n, check_qnum(s), Save/Load overloads, _Save/_Load, clone() const, get_fermion_parity, is_fermionic, print_info, ==/!=); pybind/symmetry_py.cpp is unchanged; no public method dropped. Symmetry_base/_impl are @cond-internal with no external references.
  • Finding What is braket_form? #1 impact is negligible in practice: no .n() = mutation exists anywhere in src/include/pybind, and pybind exposes n() as an int copy to Python, so the aliased-mutation-through-n() path is never exercised. It remains worth a one-line note next to the is() note, but it's cosmetic.

Net: the two nits that stand are #1 (document the n() value-semantics note) and #2 (int& n() const wart, follow-up). Verdict unchanged and if anything firmer — approve-with-nits once BuildAndTest is green.

Posted by Claude Code on behalf of @pcchen

@pcchen

pcchen commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

On @gemini-code-assist's 6 const-correctness suggestions

They're all one consistent theme (mark check_qnum, check_qnums, the combine_rule vector overload, both combine_rule_ overloads, and reverse_rule_ as const), and they're sound and safe to apply:

  • They compile — the per-kind struct methods (check_qnum/combine_rule_/reverse_rule_) are already const (Symmetry.hpp:83–89 etc.), so visiting a const kind_variant and calling them is valid. These Symmetry-level methods genuinely don't mutate *this (the visit lambdas take const auto& kind), so const is the correct qualifier.
  • They don't break the test pins — the only static_assert (Symmetry_test.cpp:20) pins n()'s int& return, not the const-ness of these six.
  • They don't break pybindcheck_qnum/check_qnums bind via &Symmetry::check_qnum (pybind handles const-member pointers); combine_rule/reverse_rule bind via lambdas calling on a non-const self. Symmetry is no longer polymorphic, so there are no overrides to worry about.
  • Source-compatible — adding const only relaxes callability (const objects can now call them); it breaks no existing caller.

One caveat: these six were non-const in the old code too (a legacy accident — the old virtual methods were non-const), so applying them is a small, deliberate deviation from this PR's "signature-by-signature identical" goal. Since it's a strict, non-breaking improvement, I'd take it — either here or as a quick follow-up. If applied:

  1. Do all six together (they're internally consistent).
  2. Confirm get_fermion_parity is already const (it's not in Gemini's list) so the read-only surface is uniformly const.
  3. Update the "signature-by-signature identical" note to "…plus const-correctness on the non-mutating rule methods."

Not a correctness bug either way, so it doesn't change my approve-with-nits verdict — just a worthwhile cleanup.

Posted by Claude Code on behalf of @pcchen

@pcchen

pcchen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Status update: #1012 has merged — three rebase items now apply here

#1012 (FermionParitySymmetry::check_qnums rejected all qnums) merged to master as 8f69f059. Since this PR deliberately preserved that bug bit-for-bit as a pinned quirk, its rebase now needs three things — one of which is a correctness trap:

  1. Port the fix into the rewritten variant implementation. This PR rewrites src/Symmetry.cpp wholesale, so a take-mine conflict resolution would silently re-introduce the bug. The variant kind-struct's check_qnums should delegate per-element to check_qnum (the structural fix fix(symmetry): FermionParitySymmetry::check_qnums rejected all qnums #1012 landed), not carry the old always-false comparison forward.
  2. Flip the preserved-quirk pin. The test that pins check_qnums returning false for non-empty input now contradicts master and will fail post-rebase — it should assert the fixed behavior ({0,1} → true; {2}/{-1}/{0,1,2} → false).
  3. Merge the two tests/Symmetry_test.cpp files (both this PR and fix(symmetry): FermionParitySymmetry::check_qnums rejected all qnums #1012 created it — both-added conflict) and dedupe the tests/CMakeLists.txt entry (which also gained the include-order canary from fix: drop C complex.h from pybind TUs; rename macro-colliding template param #981 on master).

Standing from earlier review, unchanged: approve-with-nits verdict, Gemini's six const-correctness suggestions (endorsed — apply together, check get_fermion_parity too), the n() value-semantics doc note, and the byte-compat fixtures remain valid (the Save/Load format is untouched by #1012).

Posted by Claude Code on behalf of @pcchen

@pcchen

pcchen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Consolidated checklist to land this PR

Everything below is gathered from the review, the const-correctness thread, and the #1012 merge — nothing else is outstanding.

A. Rebase onto current master (currently conflicting) — three specific resolutions

B. Const-correctness (the six suggestions, endorsed)

  • Add const to check_qnum, check_qnums, combine_rule (vector overload), both combine_rule_ overloads, and reverse_rule_ — all six together.
  • Verify get_fermion_parity is also const so the read-only surface is uniformly const.
  • Update the "signature-by-signature identical" note to "…plus const-correctness on the non-mutating rule methods."

C. Documentation

  • One-line note for the second reference→value semantic change: sym.n() = k no longer propagates to copies — alongside the existing is() address-identity note.
  • Changelog/release-note entry for the is() + n() semantic changes.

D. After rebase

Follow-up (not this PR): retire the int& n() const mutable-ref wart (return int by value) once exact-API-preservation is no longer the constraint.

Everything else was already verified in review (byte-compat pinned against real old-format fixtures, full API surface preserved, is() migration). Once A–D are checked: approve + merge. Independent of the #1011 Scalar stack (no shared files), so landing order between them is free.

Posted by Claude Code on behalf of @pcchen

#842)

Replace the boost::intrusive_ptr<Symmetry_base> hierarchy (4 virtual
subclasses, heap allocation, refcounting) with four small value structs
held inline in a std::variant:

  Symmetry::kind_variant = std::variant<U1Symmetry, ZnSymmetry,
                                        FermionParitySymmetry,
                                        FermionNumberSymmetry>

Every public method dispatches via std::visit; per the design endorsed in
issue #842, the visit lambdas are the completeness contract: adding a
fifth alternative that does not implement the full rule set (check_qnum,
check_qnums, both combine_rule_ overloads, reverse_rule_,
get_fermion_parity, is_fermionic, print_info, stype_str) fails to
compile. This exhaustiveness guarantee is compile-time only and cannot be
pinned by a runtime test.

Each kind keeps the legacy `n` field with the value the old
implementation stored (U1: 1, Zn: n, fPar: -2, fNum: -1); it backs the
preserved `int &n() const` accessor and the Save/Load byte format. The
field is declared `mutable` so the legacy accessor (a mutable reference
handed out from a const Symmetry, exactly the mutability the shared impl
exposed) stays well-defined even for genuinely const objects, with no
const_cast. The signature itself is pinned by a static_assert in
tests/Symmetry_test.cpp; accessor cleanup is tracked with the #840-era
API follow-ups.

Public API preserved exactly: factories U1()/Zn(n)/FermionParity()/
FermionNumber(), the (stype, n) constructor and Init, stype()/
stype_str()/n(), check_qnum(s), combine_rule and the combine_rule_
out-param forms (issue #840's redesign is out of scope here),
reverse_rule(_), get_fermion_parity, is_fermionic, print_info,
operator==/!= (stype + n), Save/Load/_Save/_Load, the SymmetryType
constants, all reachable error messages, and the pybind surface
(symmetry_py.cpp unchanged). clone() is now a plain copy. The only
dropped code is the dead U1Symmetry(int) checking constructor (the
wrapper always constructed U1 with n = 1; zero callers).

Immutability audit: no in-tree code mutates a Symmetry after
construction. Init()/_Load() always rebound the impl pointer (never
mutated the shared pointee), nothing assigns through n(), and nothing
reaches into _impl except utils/is.cpp. Value copies are therefore
observationally identical to the old shared-impl copies, with one
deliberate exception: is(a, b) for Symmetry now means "same object"
(&L == &R) instead of "shares the impl pointer", so a copy is no longer
`is` its source. The only in-tree observer was the doc example
example/Symmetry/clone.cpp, updated together with its .out.

Byte compatibility is pinned by committed fixtures:
tests/test_data_base/common/Symmetry/ holds .cysym files for all four
kinds plus a .cybd Bond carrying all four symmetries, written through the
public Save API by the pre-refactor implementation at the base commit
d6dcd16 (regeneration recipe: CommonDataGen.Symmetry_gen in
tests/common_data_generator.cpp). tests/Symmetry_test.cpp loads each
fixture, checks stype/n/behavior against freshly constructed objects, and
byte-compares fresh Saves against the fixture files (magic 777:u32 +
stype:i32 + n:i32; Bond container format). The core 18 tests ran green on
the base build first, then unchanged on this refactor -- both directions
of the serialization contract hold. A bad-magic Load error-path test and
the n() signature static_assert round out the suite.

Test tally: C++ 1121 ran / 1106 passed / 11 skipped / 4 known
linalg_Test.BkUt_* failures (baseline 1101/1086/11/4 plus 20 new tests);
pytest 57 passed, unchanged.

Preserved bit-for-bit and pinned by
Symmetry.FermionParityCheckQnumsLegacyQuirk:
FermionParitySymmetry::check_qnums still compares against n = -2 and so
returns false for every non-empty input, while the singular check_qnum
uses the correct [0, 2) range. Pre-existing bug, tracked separately; do
not fix without updating that test.

Refs #842.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pcchen
pcchen force-pushed the refactor/symmetry-value-type branch from 2b82c48 to bfa0e84 Compare July 8, 2026 14:29
@pcchen

pcchen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Checklist item A implemented — rebased onto master (head bfa0e84b, now MERGEABLE); the branch is master + 1 commit, 13 files:

Sanity: no conflict markers, brace balances clean, byte-compat fixtures untouched. Fresh CI is running on bfa0e84b. Items B (const-correctness), C (docs/changelog), and D (CI green) remain with the author.

Posted by Claude Code on behalf of @pcchen

Apply the six const-correctness suggestions from review: check_qnum,
check_qnums, the vector combine_rule overload, both combine_rule_ out-param
overloads, and reverse_rule_ on the Symmetry wrapper are now const. All six
dispatch via std::visit with `const auto &kind` onto kind-struct methods that
are already const, so visiting a const _impl is well-formed; get_fermion_parity
and is_fermionic were already const, making the read-only rule surface
uniformly callable on const Symmetry objects. pybind bindings are unaffected
(const member pointers / lambdas on non-const self).

This is a deliberate, non-breaking addition on top of the PR's
signature-preservation goal (const-qualification only relaxes callability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pcchen

pcchen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Checklist item B implemented (commit f60ff7de):

  • The six non-mutating rule methods on the Symmetry wrapper are now const: check_qnum, check_qnums, the vector combine_rule, both combine_rule_ out-param overloads, and reverse_rule_. All six dispatch via std::visit with const auto &kind onto kind-struct methods that were already const, so visiting a const _impl is well-formed.
  • Verified get_fermion_parity and is_fermionic were already const — the read-only rule surface is now uniformly callable on const Symmetry objects.
  • Verified the two live pybind bindings (&Symmetry::check_qnum / check_qnums) accept the const member pointers unchanged; no member-pointer caller elsewhere.
  • PR description's "Preserved exactly" note updated with the deliberate const-qualification exception.

Remaining: C (changelog entry for the is()/n() semantic changes) and D (fresh CI green — now running on f60ff7de).

Posted by Claude Code on behalf of @pcchen

…-decisions

The value-type refactor has two observable reference->value changes; only
is() was noted. Add the second alongside it: the legacy `int &n() const`
reference now points into this Symmetry's own variant, so writing through it
no longer propagates to copies (under the shared intrusive-ptr impl it did).
Documented at the accessor and recorded, together with the is() change and
the byte-format preservation, as an entry in docs/dev/api-decisions.md
(the repo has no CHANGELOG file; api-decisions.md is the release-notes
source for API-semantics changes per #1006).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pcchen

pcchen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Checklist item C implemented (commit 15b2d99e):

  • In-code note at n(): documents the second reference→value change — the returned reference now points into this Symmetry's own variant, so Symmetry b = a; a.n() = k; no longer changes b (it did under the shared intrusive-ptr impl) — placed alongside the existing legacy-API comment.
  • Release-note entry: the repo has no CHANGELOG file, so per the docs: record 2026-07 API decisions and archive the phase execution plans #1006 convention the entry went into docs/dev/api-decisions.md ("Consensus positions" list): Symmetry as a plain value type, byte-format preservation, and both observable semantic changes (is() address identity + n() copy independence), referencing this PR.
  • PR description updated: the n() note added to "Deliberate notes", and the now-stale "two pre-existing bugs preserved bit-for-bit" claim corrected (the check_qnums quirk was fixed on master by fix(symmetry): FermionParitySymmetry::check_qnums rejected all qnums #1012 and this branch ports that fix; the NEED_GEN_COMMON_DATA=1 exit crash remains the one preserved item).

Checklist status: A ✅ B ✅ C ✅ — only D remains (fresh CI green, running on 15b2d99e). Once green, this should be ready to approve and merge.

Posted by Claude Code on behalf of @pcchen

@pcchen pcchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The value-type refactor was verified in review (index-aligned variant pinned by static_asserts; Save/Load byte format pinned against real old-format fixtures generated at the base commit, byte-compared both directions; public API preserved signature-for-signature — with the one deliberate, documented exception of const-qualifying the non-mutating rule methods). The full review checklist is complete:

  • A — rebased onto master with the #1012 fix ported into the variant check_qnums (avoiding silent bug re-introduction), the preserved-quirk pin flipped to the fixed expectations, the two Symmetry_test.cpp suites merged, and the CMakeLists entry deduped.
  • B — the six const-correctness suggestions applied (+ get_fermion_parity/is_fermionic verified already const); pybind member-pointer bindings unaffected.
  • C — the n() copy-independence change documented at the accessor and recorded with the is() change in docs/dev/api-decisions.md; PR description updated (including correcting the now-stale preserved-quirk claim).
  • D — CI fully green on 15b2d99e (16 pass; the single ubuntu failure was a conda env-setup infra flake, passed on rerun).

Note for the record: the checklist commits (A–C) were implemented by Claude Code on this branch per the review threads; the core refactor is @yingjerkao's.

Posted by Claude Code on behalf of @pcchen

@pcchen
pcchen merged commit 69e73b1 into master Jul 9, 2026
21 of 22 checks passed
@pcchen
pcchen deleted the refactor/symmetry-value-type branch July 9, 2026 06:16
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.

Refactor Symmetry to a plain value type (drop PIMPL + virtual dispatch)

2 participants