Skip to content

basen/base128: name the varint continuation-bit layout - #1612

Merged
sergey-shandar merged 2 commits into
mainfrom
claude/basen128-continuation-bits
Aug 16, 2026
Merged

basen/base128: name the varint continuation-bit layout#1612
sergey-shandar merged 2 commits into
mainfrom
claude/basen128-continuation-bits

Conversation

@sergey-shandar

Copy link
Copy Markdown
Contributor

Implements fjs/basen/base128/todo/continuation-bit-layout.md (deleted here).

The problem

encode and decode in fjs/basen/base128/module.f.mjs are exact inverses, but each spelled the varint layout with its own literals and nothing tied them together:

  • encode: uint & 0x7fn, 0x80n | item, uint >>= 7n
  • decode: (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 0x7f on one side against 0x7f on the other and confirm by hand that 0x80 is the bit just above the 7-bit payload.

The change

Name the layout once and route both halves through it:

const payloadBits = 7n
const payloadMask = mask(payloadBits)
const continuationFlag = 1n << payloadBits

Deviation from the TODO

The TODO proposed literal constants (payloadMask = 0x7fn, continuationFlag = 0x80n). I derived them from payloadBits instead, via mask from types/bigint and 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's if (byte < 0x80n) became if ((byte & continuationFlag) === 0n). These are equivalent because pop8 = popFront(8n) yields exactly 8 bits, so byte < 0x80n and "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's result === 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.
  • Differential against main: 741 values — every 1n << k minus one, 1n << k, and 1n << k plus 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 concat that bit_vec does not export at top level — it lives under msb). 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 hard SyntaxError at 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 shared
    by encode and decode instead of being spelled with literals in each

No public API change — the three constants are module-private, and encode/decode keep their signatures and behavior.


Generated by Claude Code

`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
@chatgpt-codex-connector

Copy link
Copy Markdown

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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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 o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit 42635bb Aug 16, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/basen128-continuation-bits branch August 17, 2026 05:34
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.

3 participants