Encode integral SchemaBinary numbers as varints - #7368
Merged
tim-smart merged 1 commit intoAug 20, 2026
Conversation
A `Number` field spent eight bytes on every value, including `42` and `2`. Attributing the benchmark payloads showed a third of the small record and 78% of the collections case going to f64 for values that are integral. A `Number` now takes whichever of two forms is smaller, and the enclosing length says which: eight bytes are f64, one to seven bytes are a varint. The varint is capped at seven bytes so the two can never be confused; f64 is exact for integers well past that cap, so nothing is lost above it. Where no length is available the container defines the form: a uniform number array writes one mode byte and then a run in a single form, and a tuple slot keeps its length prefix. The varint is sign-magnitude rather than plain zigzag: the low bit is the sign and the rest is the magnitude, which gives `-0` a code of its own instead of needing an f64 escape for the one value zigzag cannot express. When the encoded-side schema proves the value is an integer (`Schema.Int`, `Schema.Natural`, any `isInt` check) the layout drops the f64 form and writes a bare varint, so an integer array costs neither a mode byte nor a length prefix per element. A checked number and an unchecked one are therefore different wire layouts for the same value. This is a wire change to an unstable, unreleased module. On the benchmark cases the payloads go 72 -> 58, 347 -> 318, 2553 -> 1452 and 31486 -> 27646 bytes, which puts SchemaBinary ahead of Msgpack on all four sizes. Encode and decode throughput are unchanged within run-to-run noise. Verified with a differential harness against the previous head over 27 shapes and 400 randomized values each: both implementations decode to the same value, the new one round-trips exactly across the whole number domain, the streaming parser agrees with the one-shot codec, framing errors behave identically, and single-byte corruption introduces no new failure mode.
🦋 Changeset detectedLatest commit: 9999978 The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
tim-smart
added a commit
that referenced
this pull request
Aug 21, 2026
tim-smart
added a commit
that referenced
this pull request
Aug 21, 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.
Stacked on #7366 (base branch
agent/codex-engineer/58cacc24, headc0945662b). Review the top commit only.Closes EFF-778
What changed
A
SchemaBinaryNumberspent eight bytes on every value, including42and2. Attributing the benchmark payloads showed a third of the small record and 78% of the collections case going to f64 for values that are integral.A
Numbernow takes whichever of two forms is smaller, and the enclosing length says which:Capping the varint at seven bytes is what keeps the two apart, and f64 is exact for integers well past that cap, so nothing is lost above it. Where no length is available the container defines the form: a uniform number array writes one mode byte and then a run in a single form, and a tuple slot keeps its length prefix.
The varint is sign-magnitude rather than plain zigzag: the low bit is the sign and the rest is the magnitude. That gives
-0a code of its own, so the varint form covers every integral JavaScript number instead of needing an f64 escape for the one value zigzag cannot express.NaN, the infinities, fractional values and integers past the cap stay f64.When the encoded-side schema proves the value is an integer (
Schema.Int,Schema.Natural, anyisIntcheck) the layout drops the f64 form and writes a bare varint, so an integer array costs neither a mode byte nor a length prefix per element. A checked number and an unchecked one are different wire layouts for the same value: switching a field betweenSchema.NumberandSchema.Intis a break, like any other field type change.This is a deliberate wire change to an unstable, unreleased module.
Sizes
SchemaBinary is now ahead of Msgpack on all four payload sizes. Encode and decode throughput are unchanged within run-to-run noise;
benchmark/schema/SchemaBinary.mdcarries the refreshed tables.Verification
-0,NaN, the infinities, fractional values, the safe-integer limits, and malformed number payloads, across structs, arrays, tuples, unions, the streaming parser, and schema-proven integer layouts.c0945662bover 27 shapes and 400 randomized values each: both implementations decode to the same value, the new one round-trips exactly across the whole number domain, the streaming parser agrees with the one-shot codec, framing errors behave identically, and single-byte corruption introduces no new failure mode. Total corpus size fell 14.7%.effectsuite.One pre-existing finding, unchanged by this commit and not fixed here: corrupting the int64 payload of a
Schema.DateTimeUtcmakesDateTime.makeUnsafethrow aRangeErrorinstead of failing with aSchemaError.🤖 Generated with Claude Code