Skip to content

The int_relative domain, terminal failure, min <= max on int128, and the shared corpus - #3

Merged
gafferongames merged 1 commit into
mainfrom
standard-1.1
Sep 4, 2026
Merged

The int_relative domain, terminal failure, min <= max on int128, and the shared corpus#3
gafferongames merged 1 commit into
mainfrom
standard-1.1

Conversation

@gafferongames

Copy link
Copy Markdown
Contributor

Brings serialize.java into conformance with the amended wire standard —
STANDARD.md and conformance/ at
mas-bandwidth/serialize@7e42c0f,
the commit that merged serialize#111. No wire change: every accepted vector and
every golden pin still round-trips byte for byte.

Tagged for release as v1.1.0.

The corpus test, red then green

ConformanceTests runs every vector in the vendored conformance/ directory
through ReadStream. It is the negative control for the int_relative and
int128 fixes: run against the reader as it stood before this branch, on the
same vendored corpus, it is red.

Before:

    conformance: the vendored corpus is present
    conformance int128.txt: the file holds vectors
    conformance int128.txt: degenerate-range-zero-bits
        FAILED (threw java.lang.AssertionError: null)
            at serialize.ReadStream.serializeInt128(ReadStream.java:169)
    conformance int_relative.txt: the file holds vectors
    conformance int_relative.txt: one-bit-refuse-past-domain
        FAILED: must refuse
    conformance int_relative.txt: one-bit-accept-at-domain-top
    conformance int_relative.txt: bounded-3-refuse-past-domain
        FAILED: must refuse
    conformance int_relative.txt: bounded-3-accept-at-domain-top
    conformance int_relative.txt: bounded-5-refuse-past-domain
        FAILED: must refuse
    conformance int_relative.txt: bounded-5-accept-at-domain-top
    conformance int_relative.txt: bounded-9-refuse-past-domain
        FAILED: must refuse
    conformance int_relative.txt: bounded-9-accept-at-domain-top
    conformance int_relative.txt: bounded-13-refuse-past-domain
        FAILED: must refuse
    conformance int_relative.txt: bounded-13-accept-at-domain-top
    conformance int_relative.txt: bounded-17-refuse-past-domain
        FAILED: must refuse
    conformance int_relative.txt: bounded-17-accept-at-domain-top
    conformance int_relative.txt: absolute-refuse-not-increasing
        FAILED: destination unwritten after a refusal: got 2147483647, expected 1583242846
    conformance int_relative.txt: absolute-accept-one-above-previous
    conformance int_relative.txt: absolute-refuse-top-bit-set
        FAILED: destination unwritten after a refusal: got -2147483648, expected 1583242846
    conformance int_relative.txt: absolute-refuse-all-bits-set
        FAILED: destination unwritten after a refusal: got -1, expected 1583242846
    conformance int_relative.txt: absolute-accept-domain-maximum

106 tests, 2149 checks, 10 failed
TESTS FAILED

Ten failures, and each is a distinct defect. The six refuse-past-domain
vectors were accepted: the reader reconstructed previous + difference in an
int and let it wrap past the domain top. The three absolute-refuse-*
vectors were refused for the wrong reason — the raw 32 bits were read signed,
so 0x80000000 arrived as a negative number that failed the ordering check
after it had already been written to the caller's cell. The int128 vector threw
under -ea on assert min.compareTo( max ) < 0, and would have called the
1-to-32-bit group primitive with zero bits without it.

After, on the same corpus:

    conformance: the vendored corpus is present
    conformance int128.txt: the file holds vectors
    conformance int128.txt: degenerate-range-zero-bits
    conformance int_relative.txt: the file holds vectors
    conformance int_relative.txt: one-bit-refuse-past-domain
    conformance int_relative.txt: one-bit-accept-at-domain-top
    conformance int_relative.txt: bounded-3-refuse-past-domain
    conformance int_relative.txt: bounded-3-accept-at-domain-top
    conformance int_relative.txt: bounded-5-refuse-past-domain
    conformance int_relative.txt: bounded-5-accept-at-domain-top
    conformance int_relative.txt: bounded-9-refuse-past-domain
    conformance int_relative.txt: bounded-9-accept-at-domain-top
    conformance int_relative.txt: bounded-13-refuse-past-domain
    conformance int_relative.txt: bounded-13-accept-at-domain-top
    conformance int_relative.txt: bounded-17-refuse-past-domain
    conformance int_relative.txt: bounded-17-accept-at-domain-top
    conformance int_relative.txt: absolute-refuse-not-increasing
    conformance int_relative.txt: absolute-accept-one-above-previous
    conformance int_relative.txt: absolute-refuse-top-bit-set
    conformance int_relative.txt: absolute-refuse-all-bits-set
    conformance int_relative.txt: absolute-accept-domain-maximum

110 tests, 2271 checks, 0 failed
ALL TESTS PASS

What changed, item by item

1. int_relative: the domain. ReadStream.serializeIntRelative now hands
every tier — the one-bit tier, the five bounded tiers and the absolute tier —
to a new acceptIntRelative, which reconstructs in a long, refuses unless
the result lies in [0, 2^31 - 1] and strictly exceeds previous, and writes
current.value only on acceptance. The absolute tier's 32 raw bits go through
Integer.toUnsignedLong, so a top-bit-set value is out of the domain rather
than a negative sequence number. A refusal is terminal (item 6).

The two tests that expected a wrapped result are replaced, not accompanied:
intRelative: gaps wider than 2^31 travel through the unsigned domain and
intRelative: reconstruction near INT32_MAX wraps in the unsigned domain are
gone. In their place, in IntRelativeTests:

  • intRelative: the whole domain travels, from zero to the top — write, measure
    and read at the domain edges, including 0 -> 2^31 - 1 through the absolute
    tier.
  • intRelative: a reconstruction past the domain is refused in every tier — six
    differences, one per tier, each with the corpus's discriminating shape: the
    same bytes refused one step past the domain and accepted one step inside it,
    with the destination checked unwritten on the refusal.
  • intRelative: the absolute tier reads its 32 raw bits unsigned0x80000000
    in the absolute tier is refused and writes nothing.

2. The corpus. conformance/int128.txt and conformance/int_relative.txt
are vendored verbatim from the core commit. test/serialize/tests/ConformanceTests.java
parses the vector format from STANDARD.md's "The vector format" and runs every
record: accepted vectors must decode to the stated value and consume the stated
bits, refused vectors must be refused with the destination left at a sentinel.
No expectation is regenerated, and an operation with no runner fails the suite
rather than being skipped, so a vector file added upstream cannot go unrun.

3. STANDARD.md. Vendored at the repository root, verbatim from the core
commit — this repository had no copy. The new spec-sync CI job compares it
against upstream main with the same wording the other ports use, and a second
step does a full diff -ru of conformance/ against a shallow clone, so an
added, removed or edited vector file fails the job.

4. Ranged int128. min <= max on all three streams
(ReadStream, WriteStream, MeasureStream), replacing the strict
min.compareTo( max ) < 0. A degenerate range now short-circuits on both
sides: the writer returns before writeGroups128, and the reader assigns min
and returns before readGroups128, so the zero-bit case never reaches the
1-to-32-bit group primitive in either direction. The measure already added
zero. compressed_float is untouched and keeps min < max.
Test: int128: a degenerate range is legal and costs zero bits, on the corpus
vector's bounds (2^100 + 7), checking that the writer emits nothing, the
measure adds zero, and the reader takes the value from min off an empty
stream — and it runs in both build shapes (item below).

5. Non-mutation. The int_relative absolute tier was the one assign-then-check
site; it is gone. Every other refusal in ReadStream already checked before
assigning. The corpus test asserts non-mutation on every refused vector, and the
terminality tests assert it after every latched failure. USAGE.md and the
BitStream / ReadStream class comments now state the rule and both its
limits: a caller-owned buffer (serializeBytes, serializeString,
serializeWideString) is unspecified after a refusal, and a composite read may
leave earlier members written.

6. Terminality, by latch. ReadStream carries a failed flag. Every read
entry point checks it first and returns false without touching the reader, and
every refusal sets it through a fail() helper; reset(...) clears it, and
isFailed() reports it. The flag rather than a poisoned position, because the
zero-bit paths (serializeInt on a degenerate range, serializeInt128,
serializeFixed) never consult the past-end check and would otherwise succeed
on a failed stream. Tests in StreamTests:

  • terminality: a refusal latches, and every later read fails and writes nothing
    — six failure kinds: past the end before any consumption, past the end after
    partial consumption, an offset above a range's headroom, nonzero alignment
    padding, a malformed string payload, and an int_relative read past the domain.
    After each, a one-bit read, a zero-bit read and a bool read must all fail
    and leave their cells untouched, and no bits may be consumed.
  • terminality: reset clears the latch.

7. Checked builds. WriteStream and MeasureStream gained
assert previous >= 0 on serializeIntRelative, and ReadStream asserts it
too: previous is caller state that never arrives off the wire, and with
previous < current.value already asserted, the pair pins both ends of the
domain.

8. Docs. README and USAGE.md now say what the page says, in this library's
words: the domain and per-tier reconstruction check for int_relative, min <= max
and the zero-bit degenerate range on every ranged width, the latch and what
clears it, non-mutation and its two limits, and the corpus. The STANDARD.md
links point at the vendored copy. Nothing left that contradicts the page — the
"bounds must satisfy min < max strictly" sentence and the "strictly increasing
unsigned 32-bit sequences" description are both gone.

9. Version. This repository carried no version anywhere — only the v1.0.0
tag — so one is introduced: SerializeUtil.VERSION, set to 1.1.0, with README
naming it. The release tag for this round is v1.1.0.

The assertions-disabled run

The audit's note was right that the suite refused to run with assertions off,
and the reason was sound but too broad: a forgotten -ea would have passed
silently while testing nothing on the write side. That guard is kept and
narrowed rather than removed — the suite still refuses an accidental
assertions-off run, and now accepts an explicit one, AllTests --release, which
in turn refuses to run with -ea. make test-release is the new target,
make runs both shapes, and CI runs both as separate steps.

It is worth having on its own terms: every obligation STANDARD.md places on a
reader is a check, never an assert, and this run is what proves it. A refusal
that only held under -ea would show up here as an accepted stream. Both shapes
are green at 110 tests, 2271 checks.

Gates

make (both shapes) is green locally on macOS/arm64. The repository's pinned
JDK 21 is not on this machine, so the local runs used a JDK 26 with the
Makefile's JDK_HOME override, at the same --release 17 language level and
with -Xlint:all -Werror; CI on this PR is the run on the pinned toolchain.

🤖 Generated with Claude Code

…the shared corpus

STANDARD.md and conformance/ are now vendored here, verbatim from
mas-bandwidth/serialize, with CI jobs that fail when either drifts. The
suite runs every vector in the corpus through ReadStream: an accepted
vector must decode to the stated value and consume the stated bits, a
refused vector must be refused with its destination unwritten.

int_relative reconstructs current in a long, in every tier, and refuses
the read unless the result lies in the domain (0 to 2^31 - 1) and
strictly exceeds previous. The absolute tier's 32 raw bits are read
unsigned, so a top-bit-set value is refused rather than accepted as a
negative sequence number, and no tier writes its destination before the
checks pass. The write and measure sides assert previous in the domain.

ReadStream carries a failure latch. The first refusal sets it and every
later read returns false, consuming no bits and writing no destination —
zero-bit reads included, which the past-end check alone cannot catch.
Only reset() clears it, and isFailed() reports it.

int128 takes min <= max on all three streams, as every other ranged
operation does. A degenerate range costs zero bits: the writer emits
nothing, the reader consumes nothing and takes the value from min, and
the zero-bit path no longer reaches the 1-to-32-bit group primitive.

make test-release runs the whole suite with assertions off, beside the
checked run, proving every read-side refusal is a check rather than an
assert. Both are CI gates.

Version 1.1.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gafferongames
gafferongames merged commit 6c47905 into main Sep 4, 2026
3 checks passed
@gafferongames
gafferongames deleted the standard-1.1 branch September 4, 2026 07:48
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