We already have the two mechanisms Rust's CompressorContext uses — but only in a generic form,
not the scheme-specific declarative shape Rust has, and I suspect we're "rediscovering" at each
recursion level what a scheme could just declare once.
What we already have (writer/src/main/java/io/github/dfa1/vortex/writer/encode/CascadingCompressor.java,
EncodeContext.java):
- Depth limit:
ctx.allowedCascading(), decremented on descend, checked before accepting a
non-terminal CascadeStep (CascadingCompressor.java:261,298).
- An exclusion set:
ctx.excluded(), checked against every candidate's encodingId() before it's
even sampled (CascadingCompressor.java:199,230,294).
How it's used today — generically, not scheme-declared: when an encoding like Dict recurses
into a child slot, CascadingCompressor excludes the winning encoding itself from that child's
competition (see the comment at DictEncodingEncoder.java:110-112: "excludes the winning
vortex.dict from that child's competition, so the pool is never wrapped in a second dict"). That's
a reasonable default, but it's a single hardcoded rule inside the compressor, not something each
scheme states for its own child.
What Rust does instead (vortex-compressor in spiraldb/vortex, per the blog post linked from
the README's "See also" / docs/explanation.md): each scheme builds its own exclusion list for
its child, naming exactly what doesn't make sense to re-try:
// Don't re-apply dictionary to the codes, and Sequence adds indirection without compressing.
let new_excludes = vec![IntCode::Dict, IntCode::Sequence];
let compressed_codes = compressor.compress(codes, ctx.descend(), &new_excludes);
Note the two exclusions: not just "don't re-apply myself" (Dict), but a second, unrelated
scheme (Sequence) that the author knows adds indirection without compressing dict codes
specifically. Our generic "exclude the winner" rule can't express that second exclusion — Sequence
would still compete on dict codes today, unless we've separately hardcoded it away somewhere (worth
checking SequenceEncodingEncoder.accepts/dtype gating while investigating).
Ask: audit every encoding that overrides encodeCascade
(Alp/Constant/Dict/Fsst/FrameOfReference/DateTimeParts/Patched/Ext/Sequence/SparseEncodingEncoder)
for the child-shape it produces, and give each one its own declared exclusion list for that child —
mirroring Rust's per-scheme new_excludes — instead of relying on CascadingCompressor's one
generic "exclude yourself" default. Look for:
- known-nonsensical combinations we're not currently excluding (Sequence-on-dict-codes is the one
Rust calls out explicitly; there may be others — Sparse-on-Sparse, RunEnd values re-wrapped in
RunEnd, etc.)
- whether the sampling work itself gets needlessly repeated across recursion levels for
candidates that a scheme-specific exclusion would have skipped outright (the "rediscovering it
every time" cost, not just a correctness/ratio concern)
This is a design/investigation ticket, not a scoped bug — the right first step is probably an ADR
(similar to how the cascading compressor's original design got one) rather than jumping straight to
code.
We already have the two mechanisms Rust's
CompressorContextuses — but only in a generic form,not the scheme-specific declarative shape Rust has, and I suspect we're "rediscovering" at each
recursion level what a scheme could just declare once.
What we already have (
writer/src/main/java/io/github/dfa1/vortex/writer/encode/CascadingCompressor.java,EncodeContext.java):ctx.allowedCascading(), decremented on descend, checked before accepting anon-terminal
CascadeStep(CascadingCompressor.java:261,298).ctx.excluded(), checked against every candidate'sencodingId()before it'seven sampled (
CascadingCompressor.java:199,230,294).How it's used today — generically, not scheme-declared: when an encoding like Dict recurses
into a child slot,
CascadingCompressorexcludes the winning encoding itself from that child'scompetition (see the comment at
DictEncodingEncoder.java:110-112: "excludes the winningvortex.dictfrom that child's competition, so the pool is never wrapped in a second dict"). That'sa reasonable default, but it's a single hardcoded rule inside the compressor, not something each
scheme states for its own child.
What Rust does instead (
vortex-compressorin spiraldb/vortex, per the blog post linked fromthe README's "See also" /
docs/explanation.md): each scheme builds its own exclusion list forits child, naming exactly what doesn't make sense to re-try:
Note the two exclusions: not just "don't re-apply myself" (Dict), but a second, unrelated
scheme (Sequence) that the author knows adds indirection without compressing dict codes
specifically. Our generic "exclude the winner" rule can't express that second exclusion — Sequence
would still compete on dict codes today, unless we've separately hardcoded it away somewhere (worth
checking
SequenceEncodingEncoder.accepts/dtype gating while investigating).Ask: audit every encoding that overrides
encodeCascade(
Alp/Constant/Dict/Fsst/FrameOfReference/DateTimeParts/Patched/Ext/Sequence/SparseEncodingEncoder)for the child-shape it produces, and give each one its own declared exclusion list for that child —
mirroring Rust's per-scheme
new_excludes— instead of relying onCascadingCompressor's onegeneric "exclude yourself" default. Look for:
Rust calls out explicitly; there may be others — Sparse-on-Sparse, RunEnd values re-wrapped in
RunEnd, etc.)
candidates that a scheme-specific exclusion would have skipped outright (the "rediscovering it
every time" cost, not just a correctness/ratio concern)
This is a design/investigation ticket, not a scoped bug — the right first step is probably an ADR
(similar to how the cascading compressor's original design got one) rather than jumping straight to
code.