Skip to content

Redesign CascadingCompressor's exclusion list to be scheme-declared, following Rust's CompressorContext #410

Description

@dfa1

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions