fix(filter): unblock Tests (core-and-rest) — internally tagged recursive enum stalls rustc - #784
Open
ohdearquant wants to merge 2 commits into
Open
fix(filter): unblock Tests (core-and-rest) — internally tagged recursive enum stalls rustc#784ohdearquant wants to merge 2 commits into
ohdearquant wants to merge 2 commits into
Conversation
`FilterExpression` is internally tagged (`#[serde(tag = "type")]`) and
recursive: `And`/`Or`/`Not` carried `Vec<FilterExpression>` and
`Box<FilterExpression>` as newtype variants. serde's derive routes an
internally tagged newtype variant through `TaggedSerializer<S>`, so a
self-referential newtype variant asks the compiler for
`TaggedSerializer<TaggedSerializer<...>>` with no bound. Building the crate's
test target spins in type inference rather than failing fast; the
`recursion_limit` escalations (2048, then 4096) only moved the wall further
out.
The same shape is a runtime error independent of the build: serde cannot
serialize an internally tagged newtype variant whose payload is a sequence,
so `And` and `Or` values could never be encoded ("cannot serialize tagged
newtype variant ... containing a sequence").
Convert the three logical variants to struct variants (`And { exprs }`,
`Or { exprs }`, `Not { expr }`). Struct variants serialize their fields
directly through the outer serializer, so no wrapper type nests and the
encoding of the comparison variants is unchanged. The `and`/`or`/`not`
builders keep their signatures, so callers constructing through them are
unaffected.
The crate now builds at the default recursion limit, so the explicit
`recursion_limit` attribute is dropped. Adds a round-trip test over a nested
and/not/or expression, which previously failed at serialization time.
This was referenced Aug 3, 2026
Open
…ize note
`test_logical_operator_round_trip` asserted only the root tag and the field
set, so it would still have passed with a nested variant encoded under the
wrong field name. It now compares `serde_json::to_value` against the complete
expected nested object and re-checks that the decoded value re-encodes to the
same object, which pins `{"type":"and","exprs":[...]}`,
`{"type":"not","expr":{...}}` and `{"type":"or","exprs":[...]}`.
The workspace `RUST_MIN_STACK` comment justified itself with
`ruvector-filter`'s `#![recursion_limit]` and the trait-resolution recursion
behind it, both of which this branch removes. The comment now records that,
and records the measurement: `cargo test -p ruvector-filter` builds and passes
with `RUST_MIN_STACK=2097152`, well under the 8 MB default. The setting itself
is left in place, since nothing here measures whether the rest of the
workspace still needs it.
ohdearquant
marked this pull request as ready for review
August 3, 2026 17:46
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Tests (core-and-rest)onmaindoes not fail, it runs out of clock. In the mostrecent Workspace CI run on
main(run30767906155, headcc602dc2) every other jobis green and that one job ran 21:29:04Z to 01:29:26Z and was cancelled by the
240-minute
timeout-minutes. It never reaches the test phase; it is still compilingwhen the limit hits.
The compile that never finishes is
ruvector-filter's test target.cargo check -p ruvector-filter --all-targetsfinishes in about 10 seconds;cargo test -p ruvector-filter --no-runon the same tree does not finish. Sampling the runningrustc(three independent samples onmain) shows a single thread at ~98% CPU with39k-60k
rustc_infer::infer::relate::generalize::Generalizer/structurally_relate_tysframes on the stack. It is a type-inference blowup, not slowcodegen.
Cause
FilterExpressionis internally tagged and recursive:serde's derive serializes an internally tagged newtype variant by wrapping the
outer serializer in
serde::private::ser::TaggedSerializer<S>. When the newtypepayload transitively contains
Self, the instantiation isTaggedSerializer<TaggedSerializer<...>>with no bound, so the compiler walks a typetower that only stops at
recursion_limit. The crate already carried#![recursion_limit = "2048"], raised to4096in April to clear anE0275(
&mut Vec<u8>: std::io::Writeoverflow while instantiating the serde_jsonSerializer). Raising the limit converted a fast error into a long compile; it did notremove the tower.
Measured on
main, one variable at a time:cargo test -p ruvector-filter --no-runmainas-isrustcheld the CPU 150s straight)expression.rs's#[cfg(test)]module stubbed outtest_serializationkept, everything else in that module removedserde_json::to_string(&filter)#[serde(tag = "type")]removed, recursion and tests unchangedThe last row is the isolating one: the recursion is fine, the internal tag is fine, the
combination of an internal tag with a self-referential newtype variant is what the
compiler cannot close.
The same shape is also a runtime error, independent of build time. serde cannot encode
an internally tagged newtype variant whose payload is a sequence:
So
FilterExpression::AndandFilterExpression::Orcould never be serialized at all.Notis not better off, only differently broken: its payload encodes as a map, so thetag is merged into it and the result carries two
typekeys, which serde_json thenrefuses to read back.
None of the three logical variants had a working round trip. The existing
test_serializationonly covered anEqvalue, so nothing caught it.The change
The three logical variants become struct variants:
A struct variant serializes its fields directly through the outer serializer, so no
wrapper type nests. The wire encoding of every comparison variant is unchanged
(
{"type":"eq","field":...,"value":...}), andand/or/notkeep their signatures,so callers that construct through the builders are untouched.
ruvector-node,ruvector-wasmand the doc examples all go through those builders and needed nochanges. Direct pattern matches on the three variants are updated in
evaluator.rs.With the tower gone the crate builds at the default recursion limit, so the
explicit
#![recursion_limit]attribute is dropped rather than lowered.test_logical_operator_round_tripis added: it asserts the encoded value of a nestedand/not/or expression against the exact expected JSON object, then decodes it and checks
that it re-encodes to the same object. On the previous shape that test fails during
serialization.
Compatibility
This is a breaking change to a public type and should be released as one.
(
FilterExpression::And(v),Or(v),Not(b)) must move to the named-field forms.The
and,orandnotbuilders are unchanged. No code in this repository outsideruvector-filteritself constructs or matches the three changed variants:ruvector-wasmand the crate's doc examples go through the builders, and the Nodebinding only uses the comparison forms, which are untouched.
document is affected:
AndandOrcould not be produced at all, and a producedNotdocument could not be read back. Comparison variants (eq,ne,gt,gte,lt,lte,range,in,match,geo_radius,geo_bounding_box,exists,is_null) are byte-identical before and after.Verification
cargo test -p ruvector-filter --no-run: no completion before, 10s after, at thedefault recursion limit.
cargo test -p ruvector-filter: 17 tests plus 2 doc-tests pass.cargo clippy -p ruvector-filter --all-targets -- -D warnings: clean.cargo fmtapplied.
cargo check -p ruvector-node --all-targetsandcargo check -p ruvector-wasm --features collections: both build.core-and-restshard argument list fromci.yml(--workspaceplus its 99--excludes) with--no-run: 227 test executables link, zero errors. Onmainthatsame job on CI never reaches the test phase.
Note on this PR's own CI
Tests (core-and-rest)will not go green on this branch alone. Two other things keepthat job from reporting: its
--excludelist never reaches cargo (#786), andruvector-delta-indexdeadlocks in a test once the job gets that far (#787). All threeare needed before the job produces a result rather than a timeout.