basen/base128: name the varint continuation-bit layout - #1612
Conversation
`encode` and `decode` are exact inverses, but each spelled the layout with its own literals: encode used `0x7fn`/`0x80n` and `7n`, decode used `0x7fn`, `7n` and a `byte < 0x80n` comparison. Nothing tied them together. Name the layout once as `payloadBits`/`payloadMask`/`continuationFlag` and route both halves through it. The masks are derived (`mask(payloadBits)`, `1n << payloadBits`) rather than written as literals, so the two halves agree structurally. decode's `byte < 0x80n` becomes `(byte & continuationFlag) === 0n`, which is equivalent because `pop8` yields exactly 8 bits. Differential over 741 values (every `1n << k` +/- 1 for k in 0..80, plus 500 pseudo-random 96-bit values) and 4 trailing-byte cases: byte-identical to main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | 6d89abd | Commit Preview URL Branch Preview URL |
Aug 16 2026, 05:52 AM |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Differential is clean: 4672 cases per side — 0/1/127/128/129, 16383/16384, every (1n<<k)-1n / 1n<<k / (1n<<k)+1n for k=0..200 (so 2^32, 2^53, 2^64, 2^128 all covered), 4000 pseudo-random bigints up to 200 bits, 18 trailing-byte remainder cases, and 7 malformed/truncated inputs (empty vec, lone 0x80, half-byte, ff ff ff). Encoded hex, decoded value, remainder and error behavior are byte-identical between origin/main and this head, with decode(encode(n)) === n and rest === empty on all 4647 roundtrips.
Constants are exactly the old literals: mask (fjs/types/bigint/module.f.mjs:196) is pure bigint arithmetic, mask(7n) === 127n === 0x7fn, 1n << 7n === 0x80n. Rewriting byte < 0x80n as (byte & continuationFlag) === 0n is safe because pop8 yields exactly 8 bits. No other 0x7f/0x80/7n shifts remain in fjs/basen/; fjs/asn.1/module.f.mjs:158-174 uses the same literals for ASN.1 length encoding — a different layout, correctly left alone.
npx tsc clean; 2914 pass / 0 fail; npm run cov 100/100/100.
One convention note: changelog/unreleased/1612.md probably shouldn't exist. changelog/README.md and AGENTS.md §5 both say entries are only for changes affecting behavior or the public API, and this PR's own body says "No public API change — the three constants are module-private, and encode/decode keep their signatures and behavior." #1609 and #1606 earned entries because each added a public export. Changelog: none looks right here.
Implements
fjs/basen/base128/todo/continuation-bit-layout.md(deleted here).The problem
encodeanddecodeinfjs/basen/base128/module.f.mjsare exact inverses, but each spelled the varint layout with its own literals and nothing tied them together:encode:uint & 0x7fn,0x80n | item,uint >>= 7ndecode:(result << 7n) | (byte & 0x7fn),if (byte < 0x80n)Three separate facts — payload width, payload mask, continuation flag — appear twice each, in two different notations. A reader has to match
0x7fon one side against0x7fon the other and confirm by hand that0x80is the bit just above the 7-bit payload.The change
Name the layout once and route both halves through it:
Deviation from the TODO
The TODO proposed literal constants (
payloadMask = 0x7fn,continuationFlag = 0x80n). I derived them frompayloadBitsinstead, viamaskfromtypes/bigintand a shift. That makes the relationship between the three structural rather than a coincidence of literals that still has to be checked by eye — which is the whole point of the TODO. Verified the derivations evaluate as expected:mask(7n) === 0x7fn,1n << 7n === 0x80n.One further change beyond a pure rename:
decode'sif (byte < 0x80n)becameif ((byte & continuationFlag) === 0n). These are equivalent becausepop8 = popFront(8n)yields exactly 8 bits, sobyte < 0x80nand "bit 7 clear" are the same test. The mask form is what makes the two halves visibly agree.I also added a short comment on
encode'sresult === empty ? 0n : continuationFlag, since "the first byte built is the last one emitted" is the non-obvious part of that line.Verification
npx tsc— clean.node fjs/module.mjs test— 2914 pass, 0 fail.npm run cov— 100.00% lines / branches / functions, all files.main: 741 values — every1n << kminus one,1n << k, and1n << kplus one for k in 0..80, plus 500 pseudo-random 96-bit values — recording encoded length, encoded bits, decoded value and remainder length for each, plus 4 cases decoding a vector with a trailing byte to check the remainder is handed back. Both sides ran to completion (745 rows of real hex data on each) and the outputs are byte-identical.The differential harness itself failed to load on its first run (it imported a
concatthatbit_vecdoes not export at top level — it lives undermsb). Worth stating because a harness that crashes identically on both sides compares two identical stack traces and reports a false match; here it was a hardSyntaxErrorat import, so it could not have silently passed. Fixed, then re-run; the numbers above are from the working version.Changelog
basen/base128: the varint continuation-bit layout is named once and sharedby
encodeanddecodeinstead of being spelled with literals in eachNo public API change — the three constants are module-private, and
encode/decodekeep their signatures and behavior.Generated by Claude Code