Skip to content

fp: round-trippable serde with golden format tests#229

Merged
JoeyBF merged 3 commits intoSpectralSequences:masterfrom
JoeyBF:fp-serde
Apr 8, 2026
Merged

fp: round-trippable serde with golden format tests#229
JoeyBF merged 3 commits intoSpectralSequences:masterfrom
JoeyBF:fp-serde

Conversation

@JoeyBF
Copy link
Copy Markdown
Collaborator

@JoeyBF JoeyBF commented Apr 7, 2026

Summary

Prequel PR to an upcoming zarr save system rework. Extracted to minimize that PR's diff and land the fp serde changes independently, with format tests.

fp crate

  • Derive Serialize/Deserialize on Fp<P>, FqVector<F>, Matrix, Subspace, QuasiInverse. Enables downstream serializers to store these types directly without hand-rolled byte formats.
  • Rework FpVector's manual serde impl to route through FqVector<Fp<ValidPrime>> so the prime is encoded in the wire format. The previous impl serialized as a bare Vec<u32> and panicked on deserialize, so it couldn't survive a round-trip.
  • aligned-vec gains the serde feature (needed for Matrix::data: AVec<Limb>).

Web frontend boundary fix (web_ext/sseq_gui/)

  • The sseq_gui web frontend reads SetClass.permanents / .classes / .decompositions as flat JS arrays of u32 (see interface/panels.js: d[0].reduce(...), d[0].indexOf(1), permanentClasses.map(rowToKaTeX)). If FpVector's serde format becomes prime-tagged, those JS sites break.
  • Fix: SetClass now carries those fields as Vec<Vec<u32>> (etc.) and converts from FpVector at the message boundary in sseq.rs. The JS wire format is unchanged, and FpVector's own serde is free to carry prime metadata.

Tests (crates/fp/tests/serde_format.rs, 177 lines)

  • Proptest round-trips for FpVector / Matrix / Subspace / QuasiInverse, 128 cases each, F_2 vectors up to 5 limbs.
  • Golden tests pinned via expect-test for p ∈ {2, 3, 5}, including F_2 vectors spanning exactly two and three limbs — catches mistakes in the multi-limb encoding.
  • To update after an intentional format change: UPDATE_EXPECT=1 cargo test -p fp --test serde_format.

Test plan

  • cargo test -p fp --test serde_format — all 11 tests pass (4 proptests × 128 cases + 7 goldens)
  • nix run ./ext#test — full CI pipeline (lint, tests, benchmarks, miri) green
  • JS frontend smoke test on an actual sseq_gui session (optional — type changes are purely at the Rust/JS boundary, and JS-side representation is unchanged)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enabled JSON (de)serialization for core data types to support persistence and interchange.
  • Bug Fixes / Safety

    • Stronger validation during deserialization to reject malformed or inconsistent inputs.
  • Tests

    • Added comprehensive round-trip and golden-format tests to lock down the serialization format.
  • Chores

    • Frontend message payloads simplified to plain arrays for improved interoperability.

JoeyBF added 2 commits April 7, 2026 18:47
Derive Serialize/Deserialize on Fp, FqVector, Matrix, Subspace, and
QuasiInverse, and rework FpVector's manual serde impl to route through
FqVector<Fp<ValidPrime>> so the prime is encoded in the wire format. This
gives a uniform representation across all primes that round-trips without
out-of-band context, which the zarr save system relies on.

The sseq_gui web frontend reads permanents / classes / decompositions as
flat JS arrays of u32 (interface/panels.js: `d[0].reduce(...)`,
`d[0].indexOf(1)`, `permanentClasses.map(rowToKaTeX)`), so SetClass now
carries those fields as Vec<u32> and converts from FpVector at the
message boundary. That keeps the JS format stable independently of
FpVector's own serde impl.

Add `tests/serde_format.rs`:
  * round-trip proptests for FpVector, Matrix, Subspace, and QuasiInverse
    over a broad spread of primes, dimensions, and limb counts (F_2
    vectors up to 5 limbs);
  * golden tests pinned via expect-test for p = 2, 3, 5, including F_2
    vectors that span two and three limbs, so any future format drift
    fails loudly and is only updatable via UPDATE_EXPECT=1.
@JoeyBF
Copy link
Copy Markdown
Collaborator Author

JoeyBF commented Apr 7, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Adds serde (Serialize/Deserialize) support across the fp crate types, enables aligned-vec's serde feature, implements custom (de)serialization for FpVector and FqVector, adds validation during Matrix deserialization, introduces serde-focused tests, and converts web GUI payloads to raw Vec<u32>.

Changes

Cohort / File(s) Summary
Dependency Configuration
ext/crates/fp/Cargo.toml
Enabled aligned-vec's serde feature (aligned-vec = { version = "0.6.4", features = ["serde"] }).
Core Type Derives
ext/crates/fp/src/field/fp.rs, ext/crates/fp/src/matrix/quasi_inverse.rs, ext/crates/fp/src/matrix/subspace.rs
Added Serialize/Deserialize derives to Fp, QuasiInverse, and Subspace.
Matrix (De)serialization
ext/crates/fp/src/matrix/matrix_inner.rs
Added Serialize derive and a manual Deserialize impl for Matrix that deserializes into a raw struct and validates stride, physical_rows, data.len(), and pivots, returning serde errors on invalid input.
Vector (De)serialization
ext/crates/fp/src/vector/inner.rs, ext/crates/fp/src/vector/fp_wrapper/mod.rs
FqVector<F> now Serialize + manual Deserialize with limb-count validation. FpVector gets custom Serialize/Deserialize via an intermediate FqVector<Fp<ValidPrime>> and a bytes round-trip reconstruction; error paths map to serde errors.
Tests — serde format
ext/crates/fp/tests/serde_format.rs
Added proptest round-trip tests, malformed-input tests asserting errors, and expect-test golden JSON format comparisons for FpVector, Matrix, Subspace, and QuasiInverse.
Web GUI payloads
web_ext/sseq_gui/src/actions.rs, web_ext/sseq_gui/src/sseq.rs
Replaced FpVector/FpSlice payload types with raw Vec<u32> (and nested variants) and updated construction to collect rows via row.iter().collect().

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Poem

🐰 I nibble bytes and hop on keys,

Serialize fields with gentle ease.
Matrices, vectors — JSON delight,
Round-trip safe by moonlit night.
Hooray! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the pull request: adding round-trip serialization support with golden format tests to the fp crate.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ext/crates/fp/src/vector/fp_wrapper/mod.rs`:
- Around line 262-276: After deserializing the intermediate FqVector in the
Deserialize impl for FpVector, validate that the deserialized limbs length meets
the expected limb count before calling v.to_bytes to avoid panics; compute the
expected number of limbs from the field characteristic and vector length (use
v.fq().number(v.len()) or the library helper that returns the required limb
count), compare it to v.limbs().len() (or equivalent accessor), and return a
serde::de::Error::custom error if the limbs array is too short; only proceed to
v.to_bytes(...) and Self::from_bytes(...) when the length check passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf629cb4-518f-4318-915d-44b681e8de92

📥 Commits

Reviewing files that changed from the base of the PR and between 22cf4a6 and f49fdce.

📒 Files selected for processing (10)
  • ext/crates/fp/Cargo.toml
  • ext/crates/fp/src/field/fp.rs
  • ext/crates/fp/src/matrix/matrix_inner.rs
  • ext/crates/fp/src/matrix/quasi_inverse.rs
  • ext/crates/fp/src/matrix/subspace.rs
  • ext/crates/fp/src/vector/fp_wrapper/mod.rs
  • ext/crates/fp/src/vector/inner.rs
  • ext/crates/fp/tests/serde_format.rs
  • web_ext/sseq_gui/src/actions.rs
  • web_ext/sseq_gui/src/sseq.rs

The derived `Deserialize` impls for `FqVector` and `Matrix` accepted any
combination of dimensions and limb data, so malformed input could build
instances whose internal invariants (`limbs.len() == fq.number(len)`,
`data.len() == physical_rows * stride`, etc.) didn't hold. Downstream
accessors like `FqVector::entry`, `FqVector::to_bytes`, `Matrix::row`, and
`Matrix::to_bytes` then panicked on bounds-checked slice indexing, which
escapes the deserialize boundary instead of surfacing as a normal serde
error.

Replace both derived impls with manual ones that:
  * accept an inert `Raw` struct via the derive,
  * validate the invariants, and
  * return a `serde::de::Error::custom` on mismatch.

The same validation protects `FpVector::deserialize`, which routes through
`FqVector<Fp<ValidPrime>>`, so its caller-visible behavior is now
"recoverable serde error on malformed input" instead of "panic on the
first accessor that touches the bad limb data".

Add regression tests covering both the FqVector limb-count check and the
Matrix stride / data-length checks.

Also switch the golden format tests to `serde_json::to_string_pretty` so
the pinned JSON is readable in the test source. This only affects test
source formatting, not the wire format.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ext/crates/fp/src/matrix/matrix_inner.rs`:
- Around line 73-81: The current validation computes raw.physical_rows *
raw.stride without checking for overflow; update the check in the
deserialization path that returns D::Error::custom to use checked_mul on
raw.physical_rows and raw.stride, return a custom error if checked_mul returns
None (overflow) and otherwise compare raw.data.len() with the computed product,
ensuring you reference the existing variables raw.data.len(), raw.physical_rows,
raw.stride and the same error pathway (D::Error::custom) so behavior and
messaging remain consistent.

In `@ext/crates/fp/tests/serde_format.rs`:
- Around line 155-164: Add a new unit test
`matrix_malformed_pivots_length_errors` in ext/crates/fp/tests/serde_format.rs
that deserializes a JSON where `columns` is 3 but `pivots` has length 2 (e.g.
pivots=[0,1]); assert that `serde_json::from_str::<Matrix>` returns an error and
that the error string contains the phrase "pivots length" to exercise the
`Matrix` deserialization validation for `pivots` (related to Matrix::deserialize
/ matrix_inner.rs pivots check).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 860da5ec-eb6e-4449-8c26-1db6c3e776d7

📥 Commits

Reviewing files that changed from the base of the PR and between f49fdce and 8fd3664.

📒 Files selected for processing (3)
  • ext/crates/fp/src/matrix/matrix_inner.rs
  • ext/crates/fp/src/vector/inner.rs
  • ext/crates/fp/tests/serde_format.rs

@JoeyBF JoeyBF merged commit 6855d0d into SpectralSequences:master Apr 8, 2026
24 checks passed
@JoeyBF JoeyBF deleted the fp-serde branch April 8, 2026 02:47
github-actions bot added a commit that referenced this pull request Apr 8, 2026
* fp: round-trippable serde with golden format tests

Derive Serialize/Deserialize on Fp, FqVector, Matrix, Subspace, and
QuasiInverse, and rework FpVector's manual serde impl to route through
FqVector<Fp<ValidPrime>> so the prime is encoded in the wire format. This
gives a uniform representation across all primes that round-trips without
out-of-band context, which the zarr save system relies on.

The sseq_gui web frontend reads permanents / classes / decompositions as
flat JS arrays of u32 (interface/panels.js: `d[0].reduce(...)`,
`d[0].indexOf(1)`, `permanentClasses.map(rowToKaTeX)`), so SetClass now
carries those fields as Vec<u32> and converts from FpVector at the
message boundary. That keeps the JS format stable independently of
FpVector's own serde impl.

Add `tests/serde_format.rs`:
  * round-trip proptests for FpVector, Matrix, Subspace, and QuasiInverse
    over a broad spread of primes, dimensions, and limb counts (F_2
    vectors up to 5 limbs);
  * golden tests pinned via expect-test for p = 2, 3, 5, including F_2
    vectors that span two and three limbs, so any future format drift
    fails loudly and is only updatable via UPDATE_EXPECT=1.

* Fix lint

* fp: validate FqVector/Matrix invariants in Deserialize

The derived `Deserialize` impls for `FqVector` and `Matrix` accepted any
combination of dimensions and limb data, so malformed input could build
instances whose internal invariants (`limbs.len() == fq.number(len)`,
`data.len() == physical_rows * stride`, etc.) didn't hold. Downstream
accessors like `FqVector::entry`, `FqVector::to_bytes`, `Matrix::row`, and
`Matrix::to_bytes` then panicked on bounds-checked slice indexing, which
escapes the deserialize boundary instead of surfacing as a normal serde
error.

Replace both derived impls with manual ones that:
  * accept an inert `Raw` struct via the derive,
  * validate the invariants, and
  * return a `serde::de::Error::custom` on mismatch.

The same validation protects `FpVector::deserialize`, which routes through
`FqVector<Fp<ValidPrime>>`, so its caller-visible behavior is now
"recoverable serde error on malformed input" instead of "panic on the
first accessor that touches the bad limb data".

Add regression tests covering both the FqVector limb-count check and the
Matrix stride / data-length checks.

Also switch the golden format tests to `serde_json::to_string_pretty` so
the pinned JSON is readable in the test source. This only affects test
source formatting, not the wire format.
github-actions bot added a commit to JoeyBF/sseq that referenced this pull request Apr 8, 2026
…#229)

* fp: round-trippable serde with golden format tests

Derive Serialize/Deserialize on Fp, FqVector, Matrix, Subspace, and
QuasiInverse, and rework FpVector's manual serde impl to route through
FqVector<Fp<ValidPrime>> so the prime is encoded in the wire format. This
gives a uniform representation across all primes that round-trips without
out-of-band context, which the zarr save system relies on.

The sseq_gui web frontend reads permanents / classes / decompositions as
flat JS arrays of u32 (interface/panels.js: `d[0].reduce(...)`,
`d[0].indexOf(1)`, `permanentClasses.map(rowToKaTeX)`), so SetClass now
carries those fields as Vec<u32> and converts from FpVector at the
message boundary. That keeps the JS format stable independently of
FpVector's own serde impl.

Add `tests/serde_format.rs`:
  * round-trip proptests for FpVector, Matrix, Subspace, and QuasiInverse
    over a broad spread of primes, dimensions, and limb counts (F_2
    vectors up to 5 limbs);
  * golden tests pinned via expect-test for p = 2, 3, 5, including F_2
    vectors that span two and three limbs, so any future format drift
    fails loudly and is only updatable via UPDATE_EXPECT=1.

* Fix lint

* fp: validate FqVector/Matrix invariants in Deserialize

The derived `Deserialize` impls for `FqVector` and `Matrix` accepted any
combination of dimensions and limb data, so malformed input could build
instances whose internal invariants (`limbs.len() == fq.number(len)`,
`data.len() == physical_rows * stride`, etc.) didn't hold. Downstream
accessors like `FqVector::entry`, `FqVector::to_bytes`, `Matrix::row`, and
`Matrix::to_bytes` then panicked on bounds-checked slice indexing, which
escapes the deserialize boundary instead of surfacing as a normal serde
error.

Replace both derived impls with manual ones that:
  * accept an inert `Raw` struct via the derive,
  * validate the invariants, and
  * return a `serde::de::Error::custom` on mismatch.

The same validation protects `FpVector::deserialize`, which routes through
`FqVector<Fp<ValidPrime>>`, so its caller-visible behavior is now
"recoverable serde error on malformed input" instead of "panic on the
first accessor that touches the bad limb data".

Add regression tests covering both the FqVector limb-count check and the
Matrix stride / data-length checks.

Also switch the golden format tests to `serde_json::to_string_pretty` so
the pinned JSON is readable in the test source. This only affects test
source formatting, not the wire format.
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.

1 participant