Skip to content

QuotientFilter read leftovers, and a byte left over refused as malformed under its own price (#343) - #348

Merged
mattlorimor merged 10 commits into
masterfrom
fix/343-quotient-read-leftovers
Nov 6, 2026
Merged

mattlorimor merged 10 commits into
masterfrom
fix/343-quotient-read-leftovers

Conversation

@mattlorimor

Copy link
Copy Markdown
Owner

Fixes #343.

All three of #343's QuotientFilter read leftovers are dealt with, following the coordinator's plan on #343, approved by Matt.

Part 1: no throwaway table

Build used to go through new QuotientFilter(1, 0.5, hash), which allocates a two-word table, and then the read replaced it. Build now calls a private constructor that shapes the filter at the payload's widths directly.

TableCost drops from three small objects to two: the table plus 128 bytes. The price and the reader both use it, so they still agree.

Measured: Build allocates the table plus 96 bytes, the same in 400 of 400 repeats per shape, under both collectors. A whole read now allocates 40 bytes less and is charged 64 less.

Test: TestBuildingTheFilterAReadMakesAllocatesOnlyTheFilterAndItsTable. Build must allocate exactly its table, plus what the constructor allocates beyond its own table.

  • The constructor's figure is measured in the test, at two shapes that must agree, so the test can't pass by holding Build to its table alone.
  • Red on 6e2e5ec: "a filter of 2 slots of 1-bit remainders allocated 176 bytes to build for a read: 136 beyond its table of 40, where the filter the constructor makes is 96".

#341's test keeps its bounds; only its comments changed. Those bounds are what force the charge down: with Build fixed but the charge still at three small objects, it failed as #343 predicted, "charged 232 bytes, 96 past the 136".

Mutation: the throwaway table back, no hash set, three small objects and one small object. All four killed.

Part 2: the 72 bytes come from the envelope, so this stops here

Measured for every StructureId, in both envelopes:

  • Every FromByteArray read allocates 112 bytes nothing charges: the MemoryStream (64) and the read's own ReadBudget (48).
  • Every envelope charges its Crc32 (24 bytes) as a 64-byte small object, 40 over.
  • QuotientFilter's 72 was 112 − 40.

No shared code changes in this PR. Matt has decided that every read must be charged at least what it allocates, and #346 takes the envelope's share and the per-structure gaps.

Part 3: the price on its own

QuotientFilter.Price was mutated on its own: 11 mutants, 4 survived.

  • P1, the price holding the run to one array: this is CountSketch, IBLT and DDSketch still copy a run whole on read (BeginWords), as QuotientFilter did before #328 #335's gap. It's now killed by a new QuotientFilter row in Read CountSketch, IBLT and DDSketch runs of words straight into storage (#335) #342's TestAPriceTakesARunOfWordsPastOneArrayAsItsReaderDoes: 2^31 slots of 5-bit remainders, a 2,147,483,664-byte table.
  • P5, reading the count as u64 at every version: no test priced a version-1 payload. The new TestAPayloadOfEveryVersionIsPricedAtWhatItsReaderCharges uses the three stored fixtures plus a filter written now. Its versions are held to PersistenceFormat.ReadableVersions.
  • P3 and P4 were a defect, not a gap. The price checked the payload's end before pricing the table, while the reader charges the table before checking the end. So a payload with one byte left over was refused for its budget under its own price, not as malformed. That's the defect C20 fixed for the IBLT.

The byte-left-over fix, for QuotientFilter and CountSketch

  • The fix: each price now checks the end after pricing the table, or the rows, where its reader charges them. A payload with nothing left over is priced as it was.

  • TestBytesLeftOver.cs, a new sweep with its roster from StructureId, in both envelopes. Every structure's sample, with one byte added and the checksum resealed, must be refused for that byte under exactly its own price.

    • Red on bf6873c: only CountSketch's rows fail, "under exactly its own price of 240,176, was refused for its budget".
    • Red on each fix reverted alone: only that structure's rows fail.
  • Per-structure tests:

    • TestQuotientFilter.TestAFilterWithAByteLeftOverIsRefusedForItUnderItsOwnPrice. Red: in the ordinary envelope, admitted at 200 and refused for its budget, needing 368.
    • TestCountSketch.TestASketchWithAByteLeftOverIsRefusedForItUnderItsOwnPrice. Red: admitted at 208, needing 296.

    These also hold each price to stopping at the byte, which the sweep can't check, because most prices don't walk to their payload's end.

Mutation:

  • QuotientFilter.Price after the fix: 15 killed, 0 survived.
  • CountSketch.Price: 17 killed, 1 survived. The survivor is "the price checks no end". It behaves exactly like the fix at read level, and it's killed by the sketch's own test.

CHANGELOG

Verification

Left for later, now #346

  • CountSketch's reader starts from new CountSketch(0.5, 0.5) and then replaces its table, the throwaway this PR removes from QuotientFilter.
  • The envelope's 112 and 40 bytes.
  • Most structures' reads allocate 8–328 bytes more than they're charged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx

mattlorimor and others added 9 commits November 6, 2026 00:47
…lter and its table (red)

#343 part (1): QuotientFilter.Build makes the filter a read fills through the
public constructor, new QuotientFilter(1, 0.5, hash), whose Shape allocates a
two-word table, and then shapes the filter again to the payload's widths, so
every read allocates a 40-byte table it throws away.

The test holds Build to exactly the table shaped to the payload plus what the
constructor allocates beyond its own table, the filter object. That figure is
measured in the test, at (1, 0.5) and (50,000, 0.001), rather than written down,
and the two shapes have to agree and be more than nothing, so the comparison
cannot pass by holding Build to its table alone. The slots each table should
have are worked out from the widths and checked against the filter built. The
shapes are #341's: the narrowest table, the widest remainder, 2^8 and 2^16
slots, and version 1's one-slot table for 32 quotient bits. Every count is in
a no-GC region, after an uncounted warm-up.

Red, on 6e2e5ec, Linux x64:

  a filter of 2 slots of 1-bit remainders allocated 176 bytes to build for a
  read: 136 beyond its table of 40, where the filter the constructor makes is 96

The guards passed: the constructor allocated 96 beyond its table at both
shapes, and every filter built had the slots its widths give.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…e it two small objects

#343 part (1). Build made the filter a read fills through the public
constructor, new QuotientFilter(1, 0.5, hash), and then shaped it again to the
payload's widths, so every read allocated the one-item filter's two-word table,
40 bytes, and threw it away. Build now calls a private constructor that shapes
the filter at the widths it is given and sets the hash, and nothing else, so
the table is made once.

Beyond the table, building it now allocates the filter object alone, 96 bytes,
so TableCost charges it as two small objects, 128, where #336 charged three for
the 136 the old Build allocated: 32 past what is allocated, under one small
object, as the InfiniFilter and MementoFilter segments are charged (#329,
#327). The reader and its price both call TableCost, so they still come to the
same sum. #341's TestAReadIsChargedWhatBuildingItsFilterAllocates keeps its
bounds; its summary and remarks now say what Build allocates.

Measured on this commit, Linux x64, in no-GC regions:

  Build, 400 repeats at each shape, every repeat the same, on the regions
  collector and on segments (DOTNET_GCName=libclrgc.so, three processors, the
  test host reporting GCName=libclrgc.so and GCRegionRange=0):
    2 slots of 1 bit       136   (table 40 + 96)
    2^16 slots of 16 bits  155,784 (table 155,688 + 96)
    version 1's one slot   136   (table 40 + 96)

  A whole read, least of seven, allocated minus charged, the same at four
  shapes from (1, 0.5) to (1,000,000, 0.0001) and in both envelopes:
    read from a stream, its budget made outside the count   -72  (was -96)
    FromByteArray under ReadOptions.Unlimited               +40  (was +16)
  Each read allocates 40 bytes less and is charged 64 less. The +40 is not the
  filter's: see #343 part (2) in the report. Every structure's FromByteArray
  read allocates a 64-byte MemoryStream and its 48-byte ReadBudget, which no
  charge names, and the envelope charges its 24-byte Crc32 as a 64-byte small
  object; 112 - 40 - 32 = 40.

Watched in the middle: with Build fixed and TableCost still at three small
objects, the new test passed and #341's failed its upper bound, as the issue
said it would:

  a filter of 2 slots of 1-bit remainders is charged 232 bytes, 96 past the
  136 building it allocated

TestBuildingTheFilterAReadMakesAllocatesOnlyTheFilterAndItsTable, red on the
parent, passes, with the rest of TestQuotientFilter.

Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit: the
full suite for each, then each scoped to the new test (NEW) and to #341's
TestAReadIsChargedWhatBuildingItsFilterAllocates (OLD):

  F1 the private constructor shapes a two-word table first, as the old Build
     did                                  killed (2 tests: NEW and OLD)
  F2 the private constructor sets no hash killed (32 tests)
  F3 TableCost three small objects        killed (1 test: OLD; NEW survives)
  F4 TableCost one small object           killed (1 test: OLD; NEW survives)

NEW holds what Build allocates and never reads TableCost, so F3 and F4 survive
it by design; OLD holds the charge to the build from both sides, F3 past its
upper bound and F4 under its lower one. F1 fails OLD's lower bound as well as
NEW: 176 allocated against 168 charged at two slots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…ay as its reader does

#343 part (3). QuotientFilter's price was mutated on its own, on 6eb7e4b's
tree (a137724, the same tree before its message was amended), through
scripts/mutate.sh with MUTATION_JOBS=2 and the full suite for each:

  P1  the price begins the table's run with BeginRun, which holds it to one
      array, not BeginWordsInPlace          SURVIVED (all 2525 passed)
  P2  the price skips no run                killed   (3 tests)
  P3  the price checks no end               SURVIVED (all 2525 passed)
  P4  the price checks the end after pricing the table
                                            SURVIVED (all 2525 passed)
  P5  the price reads the count as a u64 at every version
                                            SURVIVED (all 2525 passed)
  P6  the price reads the count as a u32 at every version
                                            killed   (5 tests)
  P7  the price checks no widths            killed   (4 tests)
  P8  the price never takes version 1's one-slot table
                                            killed   (4 tests)
  P9  the price prices no table             killed   (3 tests)
  P10 the price prices a remainder a bit wider killed (4 tests)
  P11 the price reads the two widths swapped killed  (3 tests)

P1 is the gap #335 found in the other three structures. No payload this
library writes in the suite holds a run past one array, and a refused price is
set aside: the read is admitted at what was priced by then, and its reader then
charges the table, so a budget too small for the whole is refused by the reader
instead, naming the same total. This commit kills P1. P5 is the next commit's.
P3 and P4 are not test gaps and are not killed: see the report for #343. With
either, a payload holding a byte past its table is refused for that byte under
its own price, as it should be, where this tree refuses it for its budget.

The test is #342's TestAPriceTakesARunOfWordsPastOneArrayAsItsReaderDoes with a
fourth row: a quotient filter of 2^31 slots of 5-bit remainders, a table the
constructor builds, whose 2^28 + 2 words are 2,147,483,664 bytes, past one
array, written in the large form at version 12. Its fields are where FORMAT.md
lays them out, the two widths, the count and the run's u64 length, on a stream
of zeros that claims them all. The price is walked on its own and must price
exactly TableCost, what the reader charges for the table and the filter. Passes
here.

Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit:

  P1 scoped to TestAPriceTakesARunOfWordsPastOneArrayAsItsReaderDoes
                                            killed (1 test)
  P1 against the full suite                 killed (1 test)

Hand-applied, the QuotientFilter row is the one that fails, and for the
reason it names, while the other three rows pass:

  System.IO.InvalidDataException: Payload holds a 2147483664-byte run, which
  cannot be read into a single array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…t its reader charges

#343 part (3), the price's other survivor. Mutated on its own on 6eb7e4b's
tree (the table is in the parent's message), P5, a price that reads the count
as a u64 at every version, passed the whole suite: 2525 tests. A payload of
version 1 counts in a u32, so that price reads the run's length into the
count and the table's first bytes as the run's length, and refuses the
payload; a refused price is set aside, and the read is admitted at the
envelope alone, its reader then charging the table. So a version 1 payload
read whole still loads, and only a refusal's number shows the difference.

The test takes the three stored fixtures (version 1's one-slot table,
quotientfilter-v1.bin and quotientfilter-v12.bin) and a filter written now,
and for each walks the price on its own, which must not refuse the payload
and must price exactly TableCost, what the reader charges; then reads it
whole, refused under a budget of nothing naming the envelope and that, loaded
under that total as the filter it reads as under no limit, and refused under
a byte less. The versions the payloads cover are held to
PersistenceFormat.ReadableVersions, so a version added without a payload here
fails. Passes here.

Hand-applied P5, the test fails at the one-slot table, its first payload:

  one slot, at version 1: its price refused it, so it is set aside and the
  read admitted at less: Payload ended after 32 bytes while reading 8065745
  more at offset 20. ...

and with the price's walk taken out of the test, so only the whole read is
checked, it fails on that too:

  one slot, at version 1, refused under a budget of nothing, named the wrong
  total (expected 360, actual 192)

Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit:

  P5 scoped to this test                    killed   (1 test)
  P5 against the full suite                 killed   (1 test)
  P3 the price checks no end                SURVIVED (all 2527 passed)
  P4 the price checks the end after pricing the table
                                            SURVIVED (all 2527 passed)

P3 and P4 are left alive on purpose, and are not equivalent: each is the
price this tree should have. A payload holding a byte past its table is
refused for its budget under its own price, because the price checks the end
before it prices the table, and its reader charges the table before it checks
the end. Measured on this tree's library, a filter for (1, 0.5) holding one
key, one byte added and the checksum resealed:

  as committed  admitted at 200, the envelope alone; under 200 refused for its
                budget, needing 368; under 368 refused for the byte left over
  P3 or P4      admitted at 368; under 368 refused for the byte left over

and the same at (50,000, 0.001): admitted at 213,192 and refused needing
426,352, or, with either mutant, admitted at 426,352. A test that killed P3 or
P4 would pin the refusal for the budget. It is the defect
InvertibleBloomLookupTable's price had (TESTING.md, "A price prices each array
where its reader charges it"). It is left for a Decision, not fixed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…nder its own price (red)

#343, found by mutating the price on its own (P3 and P4 in e59fb71's
message). QuotientFilter.Price checks that the payload ends after the table
before it prices the table, and its reader charges the table before it checks
the end. So a payload holding a byte past its table is priced at the envelope
alone: the price stops at the end check, it is set aside, the read is admitted
at what it had priced, and the reader charges the table and is refused for a
budget its own price fits, rather than for the byte. It is the defect
InvertibleBloomLookupTable's price had (TESTING.md, "A price prices each array
where its reader charges it").

The test is modelled on TestAnInvertibleTableWithAByteLeftOverIsRefusedForItUnderItsOwnPrice,
in both envelopes. The smallest filter the constructor builds, holding one key,
is written in each, one byte is added after the payload and the length and
checksums made to match (WithAByteLeftOver, which reseals the large form's
header checksum as well as its end checksum). The price is walked on its own
over the longer payload and must stop at the byte, for it; the read is then
read under exactly the envelope and what the walk priced, and must be refused
for the byte, not for its budget; and under a byte less it is refused for its
budget naming that price, so the price worked out here is the one the read is
admitted at.

Red on e59fb71, Linux x64, in each envelope (the large form run on its own,
since the loop stops at the first):

  in the ordinary envelope, under exactly its own price of 200, it was refused
  for its budget rather than for the byte left over: ... new ReadOptions(368)

  in the large form, under exactly its own price of 240, it was refused for
  its budget rather than for the byte left over: ... new ReadOptions(408)

The premises held in both: the price stopped at the byte, for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…hecking the end

#343. QuotientFilter.Price checked that the payload ended after the table
before it priced the table, and the reader charges the table before it checks
the end, so a payload holding a byte past its table was priced at the envelope
alone and its read refused for a budget its price fits. The price now checks
the end last, after it has priced the table, so it prices the table where the
reader charges it: once the table's run is held and the widths are checked.
A payload with nothing left over is priced as it was, at every step.

Measured on this commit, a filter for (1, 0.5) holding one key, one byte added
and its checksums resealed: admitted at 368 bytes in the ordinary envelope
(was 200) and 408 in the large form (was 240), and under each refused for the
byte left over, not for its budget, and under a byte less refused for its
budget naming that price.

TestAFilterWithAByteLeftOverIsRefusedForItUnderItsOwnPrice, red on the
parent, passes, with the rest of TestQuotientFilter*.

Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit: the
price on its own again, the full suite for each, and the reorders also scoped
to the new test, which is the one test each is killed by:

  R1 the end checked after the widths and before the price
                                        killed (1 test; the new one)
  R2 the old order exactly, the end checked before the widths and the price
                                        killed (1 test; the new one)
  R3 the price checks no end            killed (1 test; the new one)
  P1 the run held to one array          killed (1 test)
  P2 no run skipped                     killed (3 tests)
  P5 the count a u64 at every version   killed (1 test)
  P6 the count a u32 at every version   killed (6 tests)
  P7 no widths checked                  killed (4 tests)
  P8 version 1's table never wrapped    killed (5 tests)
  P9 no table priced                    killed (6 tests)
  P10 a remainder a bit wider           killed (6 tests)
  P11 the widths read swapped           killed (5 tests)

R2 is the parent's price and R1 is the same fault one statement later; both
are killed by the read under its own price being refused for its budget. R3,
hand-applied, is killed by the test's premise only ("the price stops for the
byte left over"): with no end check the price walks a payload with a byte left
over to its end and prices all of it, and the read is then refused for the byte
under that price, as it is with this commit. At the level of a read R3 is the
same; the premise holds the price to refusing what its reader refuses, as the
invertible table's test does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
One release ships both, so the one bullet now says what that release does,
measured against the last one, 6.2.0, which had no read budget: the reader
builds the filter at the payload's widths with no table to throw away, and
charges it as two small objects beside its table. The intermediate states,
charged as one small object and then as three, never shipped and are gone.
The price fix keeps its own bullet below.

6.2.0's figure was measured from its tree (git archive v6.2.0, built in a
scratch directory, outside this repository), on the same runtime, 10.0.14:
the least of seven calls of its private Build in a no-GC region, by
reflection, beyond the table each built:

  2 slots of 1 bit      160 allocated, table 40       120 beyond
  4 slots of 61 bits    192, table 72                 120
  2^8 slots of 7 bits   480, table 360                120
  2^16 slots of 16 bits 155,808, table 155,688        120

and new QuotientFilter(1, 0.5) alone, whose two-word table Build threw away,
120: its filter object then, 80, and the table, 40. On this branch Build
allocates 96 beyond its table at the same tables and at version 1's one-slot
table (6eb7e4b's message).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…is refused for it under its own price (red)

#343. Probing every structure for the quotient filter's price defect (its
price checked the payload's end before pricing its table, where its reader
charges the table first) found one more: CountSketch. Its price checks the end
before it prices its rows, and its reader charges the rows before it checks
the end, so a payload holding a byte past its cells is admitted at the
envelope alone and refused for a budget its own price fits.

Three changes, all tests:

- TestBytesLeftOver, a new file. WithAByteLeftOver (moved here from
  TestQuotientFilter) adds one byte after a payload in either envelope and
  reseals its length and checksums. AssertRefusedForTheByteUnderItsOwnPrice is
  the quotient filter's test made general: for a structure whose price checks
  the end, the price walked alone stops at the byte, for it; the read under
  exactly the envelope and what the walk priced is refused for the byte, not
  for its budget; and under a byte less it is refused for its budget, naming
  that price. And the sweep, TestEveryStructureWithAByteLeftOverIsRefusedForItUnderItsOwnPrice,
  whose roster is StructureId through TestReadBudget.Structures: every
  structure's read-budget sample, in each envelope, with a byte added, must be
  refused for the byte with no limit (so the byte is a fault), refused for its
  budget under a byte less than the total a refusal under nothing names,
  naming it (so that total is the price the read is admitted at), and refused
  for the byte under exactly that price. The sweep finds the price from the
  read and does not walk it: most prices do not walk to their payload's end
  (probed, 50 of 66 structure-and-envelope rows priced to their end or
  stopped at the fields they price from without checking it), and the reader
  checks the end.
- TestCountSketch.TestASketchWithAByteLeftOverIsRefusedForItUnderItsOwnPrice,
  the sketch at ε = δ = 0.5 holding one item, in both envelopes, through
  AssertRefusedForTheByteUnderItsOwnPrice.
- TestQuotientFilter's byte-left-over test now goes through the same
  assertion, unchanged in what it asserts.

Red on bf6873c, Linux x64:

  the sweep, CountSketch's row (32 other rows pass):
    CountSketch with a byte left over, under exactly its own price of 240,176,
    was refused for its budget rather than for the byte
  measured by the probe on the same library: in the large form, admitted at
  240 and refused needing 240,360; in the ordinary envelope needing 480,296.

  the count sketch's own test:
    in the ordinary envelope, under exactly its own price of 208, the
    CountSketch was refused for its budget rather than for the byte left
    over: ... new ReadOptions(296)
    in the large form (run first, on its own): under exactly its own price of
    240 ... new ReadOptions(328)

The quotient filter's test passes, as it did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
…cking the end

#343. CountSketch.Price checked that the payload ended after the cells before
it priced the rows, and the reader charges the rows before it checks the end,
so a payload holding a byte past its cells was priced at the envelope alone
and its read refused for a budget its price fits: the quotient filter's
defect, fixed the same way. The price now checks the end last, after it has
priced the rows, so it prices them where the reader charges them: once the
cells are held and their shape checked. A payload with nothing left over is
priced as it was.

The CHANGELOG's bullet for the quotient filter's price fix now covers both.

TestASketchWithAByteLeftOverIsRefusedForItUnderItsOwnPrice and the sweep's
CountSketch row, red on the parent, pass: TestBytesLeftOver, TestCountSketch,
TestQuotientFilter* and #342's price test, 148 of 148.

Mutation, through scripts/mutate.sh with MUTATION_JOBS=2, on this commit:
CountSketch's price on its own, the full suite for each,

  CR1 the end checked after the cells and before the price   killed (2 tests)
  CR2 the old order exactly                                  killed (2 tests)
  CR3 the price checks no end                                killed (1 test)
  C1  the run held to one array                              killed (1 test)
  C2  no run skipped                                         killed (2 tests)
  C3  no cells checked                                       killed (4 tests)
  C4  no rows priced                                         killed (5 tests)
  C5  a row more priced                                      killed (6 tests)
  C6  width and depth read swapped                           killed (4 tests)

and scoped, which test kills the reorders: CR1 and CR2 are each killed by the
sketch's own test and by the sweep; CR3 by the sketch's own test, and
survives the sweep (all 33 passed). With no end check the price walks a
payload with a byte left over to its end and prices all of it, and the read
is refused for the byte under that price, as it is with this commit: CR3 is
the same at the level of a read, which is all the sweep looks at, and the
sketch's own test holds the price to refusing what its reader refuses.

The sweep, red on each of the two price fixes reverted, one at a time:

  QR2 QuotientFilter's old order exactly, against the full suite killed
      (2 tests); against the sweep alone killed (1 test). Hand-applied, the
      sweep fails on QuotientFilter's row only, 32 rows passing:
        QuotientFilter with a byte left over, under exactly its own price of
        213,192, was refused for its budget rather than for the byte
  CR2 CountSketch's old order exactly, against the sweep alone killed
      (1 test). Hand-applied (the end checked before the price), the sweep
      fails on CountSketch's row only, 32 rows passing:
        CountSketch with a byte left over, under exactly its own price of
        240,176, was refused for its budget rather than for the byte

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
@mattlorimor
mattlorimor merged commit 2a4dfbc into master Nov 6, 2026
9 checks passed
@mattlorimor
mattlorimor deleted the fix/343-quotient-read-leftovers branch November 6, 2026 13:53
mattlorimor added a commit that referenced this pull request Nov 6, 2026
Brings in #345 and #348 (2a4dfbc). CHANGELOG merged without conflict.
#345's TestANestedEnvelopeClaimingMoreThanItsRunIsRefusedAsData fails on
this merge, as expected: it pins Slice's refusal, and the price now
refuses first, in RequireHeld. The commit after this holds it to that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
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.

QuotientFilter read leftovers: Build's throwaway table, 72 bytes uncharged, a possibly untested price

1 participant