Skip to content

types/byte_set: toRangeMap carries a boolean, not an FSM payload - #1566

Merged
sergey-shandar merged 4 commits into
mainfrom
claude/todo-implementation-rifq4g
Aug 14, 2026
Merged

types/byte_set: toRangeMap carries a boolean, not an FSM payload#1566
sergey-shandar merged 4 commits into
mainfrom
claude/todo-implementation-rifq4g

Conversation

@sergey-shandar

@sergey-shandar sergey-shandar commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Implements fjs/types/byte_set/todo/torangemap-payload.md (deleted here).

BREAKING CHANGEtoRangeMap is an exported member of fjs/types/byte_set and its signature changes incompatibly. The changelog entry is prefixed accordingly (§8.4), so the next release cannot be a patch. The only in-repo importer, fjs/fsm, is updated in this PR.

Why

Everything else in byte_set is ByteSet → ByteSet / boolean bitmask algebra. toRangeMap alone hardcoded a SortedSet<string> state-name payload:

/** @type {(n: ByteSet) => (s: string) => (i: number) => RangeMap<SortedSet<string>>} */
const toRangeMapOp = n => s => i => {  prev ? [s] : []  }

That is a DFA-construction concept, and its only caller is fsm's foldOp. A types leaf naming string and SortedSet for one higher-level consumer inverts the layering (§5.4).

What

The payload becomes the set's own answer:

/** @type {(n: ByteSet) => RangeMap<boolean>} */
export const toRangeMap = n => flat(map(toRangeMapOp(n))(counter))

byte_set drops its sorted_set dependency and stops naming string anywhere. fsm labels the ranges itself, which is where "which state does this range lead to" belongs:

/** @type {(ruleOut: string) => (entry: Entry<boolean>) => Entry<SortedSet<string>>} */
const labelRange = ruleOut => ([inSet, max]) => [inSet ? [ruleOut] : [], max]

return merge(mergeOp)(rm)(map(labelRange(ruleOut))(toRangeMap(bs)))

Deviation from the TODO's proposed signature

The TODO offered two options — move toRangeMap into fjs/fsm, or make the payload generic as (n: ByteSet) => <T>(v: T) => RangeMap<T>. I took neither verbatim:

  • Moving it to fsm would export the byte domain the other way: counter is reverse(countdown(256)), and 256 is a byte-set fact, not a DFA one. The conversion "my bitmask as ranges" is genuinely byte_set's.
  • The generic signature is one value short. The current code emits a payload for ranges outside the set as well as inside (prev ? [s] : []), so a generic version needs both whenIn and whenOut. A generic pair also infers awkwardly here: T inferred from [ruleOut] gives string[], while the sibling argument is SortedSet<string> (a List, which string[] does not accept in the other direction), so the call site would need an annotation to pin T.

boolean says the same thing with no type parameter at all, and pushes exactly one map onto the single consumer. fjs/types/todo/bit-set-factory.md's claim that "toRangeMap stays local — it is genuinely byte-specific" is now more true than before, so it needed no edit.

range_map remains a type-only import in byte_set — it is the shape of the return value, not a dependency on the container's behavior.

Verification

  • npx tsc clean.
  • fjs test: 2708 pass, 0 fail. fjs/fsm/proof.f.mjs pins run output against expected strings and passes unchanged.
  • npm run cov: both fjs/types/byte_set/module.f.mjs and fjs/fsm/module.f.mjs at 100% lines/branches/functions.
  • Differential check on the built DFA. Since the payload moved across a module boundary, I built the same grammar before and after and compared the serialized DFA: byte-identical, 308 bytes.
  • byte_set proof expectations updated to the boolean payload ([[true,0]], [[false,0],[true,2]], …) — same boundaries, same count, only the payload spelling changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs

Everything else in byte_set is bitmask algebra, but toRangeMap hardcoded a
SortedSet<string> state-name payload for its one consumer, fjs/fsm, making a
types leaf name `string` and depend on sorted_set for a DFA-construction
concept.

The payload is now the set's own answer: toRangeMap returns RangeMap<boolean>,
"is the range up to this byte in the set". byte_set drops its sorted_set
dependency and stops naming `string` at all. fjs/fsm labels the ranges itself
with a `labelRange` projection, which is where the question "which state does
this range lead to" belongs.

The todo proposed a generic `<T>(v: T) => RangeMap<T>` payload instead. That
signature is short one value -- the current code emits a payload for ranges
outside the set as well as inside -- and a generic pair infers awkwardly at
the call site. Boolean says the same thing with no type parameter at all.

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 14, 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 2c0b20d Commit Preview URL

Branch Preview URL
Aug 14 2026, 11:03 PM

@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.

Approving. The narrowing is behaviour-preserving end to end, the **BREAKING CHANGES:** prefix is correctly applied, and the moved proofs still discriminate under mutation.

Baseline for everything below: origin/main at 4a3b6877 (js/keywords: one source of truth for JavaScript keywords (#1562)).

Public surface — breaking, and correctly labelled

I read the emitted declarations rather than a name-keyed extract, and diffed every .d.mts in fjs/ between the two trees after npm run prepack. The file set is identical and exactly one file differs:

fjs/types/byte_set/module.f.d.mts
- export declare const toRangeMap: (n: ByteSet) => (s: string) => RangeMap<SortedSet<string>>;
+ export declare const toRangeMap: (n: ByteSet) => RangeMap<boolean>;

fjs/fsm/module.f.d.mts is byte-identical — labelRange stays private, so the DFA layer's surface does not move.

That is a change in both arity and payload type on an exported const, so §8.4 wants the prefix and the entry has it. This is not the #1524 situation (representation-only, identical emitted surface, correctly no prefix); it is closer to #1520, where the specifier-level break did carry one. The dropped SortedSet import and its @import line go with it, and @module survives in the emitted declaration.

Behaviour sweep — no divergence anywhere in the domain

I ran main's toRangeMap(n)(s) against the PR's map(labelRange(s))(toRangeMap(n)) — importing both trees side by side in one process — over 1710 comparisons: empty, universe, all 256 singletons, ten boundary ranges ([0,0], [0,1], [0,255], [1,255], [254,255], [255,255], [127,128], …), the alternating pattern and its complement, and 300 pseudo-random 256-bit sets, each against label strings 'a', 'zz' and ''.

byte_set checked 1710 mismatches 0

Negative control: injecting one spurious entry for a single case makes it report mismatches 3 (one per label string), so the comparison is live rather than vacuously passing.

Then the same both-trees comparison one level up, on fsm's actual exports, over seven grammars — empty, id-loop, the multi-rule float/int one, a rule with the empty byte set, a rule with the universe, and singletons at bytes 0 and 255 — comparing dfa(grammar) and run(dfa)(input) for eight inputs each including '', and ÿ:

fsm grammars 7 mismatches 0

So no divergence to classify — neither a regression nor a latent-bug fix. The old [s]/[] payload was boolean wearing a costume, and labelRange puts the costume back on at the only place that wanted it.

Proofs mutation-tested, not read (§3.2)

The four toRangeMap cases were rewritten, so I checked they still kill:

  • [[prev, i]][[!prev, i]] (invert the payload): dies — 3 of 4 byte_set cases plus 5 fsm cases.
  • current === prev ? null : [[prev, i]][[prev, i]] (drop the boundary check, emit all 256): dies — including the empty-set case.
  • fsm's new labelRange: [inSet ? [ruleOut] : [], max][[ruleOut], max] (label every range, in-set or not): diesfsm.proof.dfa and 2 run cases.

The empty-set case (assertEq(result, '[]')) is the weakest of the four on its own — it survives the inversion mutant, which is unavoidable given it produces no entries — but it is killed by the boundary mutant, so it is not dead weight.

Battery

  • npx tsc --noEmit — exit 0.
  • npm run prepack from a clean tree — exit 0.
  • npm test2709 pass / 0 fail, exactly matching 4a3b6877. A behaviour-preserving change should match, and it does.
  • bin/linkcheck.mjs — broken-link sets byte-identical to main. Worth checking here since the PR deletes fjs/types/byte_set/todo/torangemap-payload.md; nothing pointed at it, and the surviving mention in fjs/types/todo/bit-set-factory.md is prose about toRangeMap staying local, which is still true.
  • §6.2 _ prefix: no types added. Entry joins fsm's @import list, but it is an already-public export of range_map/types.ts, so no prefix question arises. labelRange is a plain private const.
  • §8.3: changelog/unreleased/1566.md is 222 characters, list items only, no heading, no PR number or link inside. Deleting a todo/ file needs no entry of its own.

The JSDoc on toRangeMap explaining why the payload is boolean and where the labelling went is the part that will save the next reader the trip through git history.

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 14, 2026
Merged via the queue into main with commit db76410 Aug 14, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/todo-implementation-rifq4g branch August 14, 2026 23:09
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