From e4f744fcc0658612c7861e828f509fa218fdf428 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 01:33:02 +0000 Subject: [PATCH 1/2] fsm: build toRange on ascii.range instead of re-implementing it Working notes; the PR title and description are the commit message. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs --- fjs/fsm/module.f.mjs | 31 ++++++++++----------- fjs/fsm/proof.f.mjs | 28 ++++++++++++++++--- fjs/fsm/todo/torange-ascii-range.md | 42 ----------------------------- 3 files changed, 39 insertions(+), 62 deletions(-) delete mode 100644 fjs/fsm/todo/torange-ascii-range.md diff --git a/fjs/fsm/module.f.mjs b/fjs/fsm/module.f.mjs index 3fc2804f7..7f9dc0b0b 100644 --- a/fjs/fsm/module.f.mjs +++ b/fjs/fsm/module.f.mjs @@ -12,13 +12,13 @@ */ import { equal, isEmpty, fold, map, toArray, foldScan, empty as emptyList } from '../types/list/module.f.mjs' -import { toRangeMap, union as byteSetUnion, one, empty, range } from '../types/byte_set/module.f.mjs' +import { toRangeMap, range } from '../types/byte_set/module.f.mjs' import { intersect, union as sortedSetUnion } from '../types/sorted_set/module.f.mjs' import { merge, get as rangeMapGet } from '../types/range_map/module.f.mjs' import { strictEqual } from '../types/function/operator/module.f.mjs' +import { range as asciiRange } from '../text/ascii/module.f.mjs' import { stringify } from '../media/json/module.f.mjs' -import { identity } from '../types/function/module.f.mjs' -import { stringToList } from '../text/utf16/module.f.mjs' +import { compose, identity } from '../types/function/module.f.mjs' import { cmp } from '../types/string/module.f.mjs' /** @typedef {readonly [string, ByteSet, string]} _Rule */ @@ -29,20 +29,17 @@ import { cmp } from '../types/string/module.f.mjs' const stringifyIdentity = stringify(identity) -/** @type {(s: string) => ByteSet} */ -export const toRange = s => { - const [b, e] = toArray(stringToList(s)) - return range([b, e]) -} - -/** @type {Fold} */ -const toUnionOp = i => bs => byteSetUnion(bs)(one(i)) - -/** @type {(s: string) => ByteSet} */ -export const toUnion = s => { - const codePoints = stringToList(s) - return fold(toUnionOp)(empty)(codePoints) -} +/** + * The byte set of an inclusive ASCII character range, written as the two + * endpoint characters: `toRange('az')`. + * + * `fjs/text/ascii` owns "two-character string to inclusive `Range`", including + * the one-character case where both endpoints are that character, so this is + * its composition with `byte_set.range` and nothing more. + * + * @type {(s: string) => ByteSet} + */ +export const toRange = compose(asciiRange)(range) /** @type {Properties>} */ const mergeOp = { union: sortedSetUnion(cmp), equal: equal(strictEqual), def: [] } diff --git a/fjs/fsm/proof.f.mjs b/fjs/fsm/proof.f.mjs index b6c674591..c3b5e3843 100644 --- a/fjs/fsm/proof.f.mjs +++ b/fjs/fsm/proof.f.mjs @@ -1,18 +1,30 @@ /** * @import { Grammar } from './module.f.mjs' + * @import { ByteSet } from '../types/byte_set/types.ts' */ -import { dfa, run, toRange, toUnion } from './module.f.mjs' -import { union } from '../types/byte_set/module.f.mjs' +import { dfa, run, toRange } from './module.f.mjs' +import { one, union, empty, range as byteSetRange } from '../types/byte_set/module.f.mjs' import { sort, fromEntries } from '../types/object/module.f.mjs' import { stringify } from '../media/json/module.f.mjs' import { identity } from '../types/function/module.f.mjs' -import { toArray } from '../types/list/module.f.mjs' +import { fold, toArray } from '../types/list/module.f.mjs' import { stringToList } from '../text/utf16/module.f.mjs' import { assertEq } from '../asserts/module.f.mjs' const stringifyIdentity = stringify(identity) +/** + * The byte set of a string's characters, used to spell a grammar's punctuation + * alphabets below. It lives here rather than in `fjs/fsm` or `types/byte_set`: + * `fjs/fsm` has no use for it, and a `types` leaf taking a *string* would have + * to depend on `fjs/text` to read one. + * + * @type {(s: string) => ByteSet} + */ +const toUnion = s => + fold((/** @type {number} */i) => (/** @type {ByteSet} */bs) => union(bs)(one(i)))(empty)(stringToList(s)) + const buildDfa = () => { const lowercaseAlpha = toRange('az') const uppercaseAlpha = toRange('AZ') @@ -39,6 +51,16 @@ const buildDfa = () => { } export const proof = { + toRange: [ + // Two characters name the inclusive range's endpoints. + () => assertEq(toRange('az'), byteSetRange([0x61, 0x7a])), + // One character is the singleton range. This threw `RangeError: The + // number NaN cannot be converted to a BigInt` while `toRange` read a + // second character that was not there. + () => assertEq(toRange('a'), byteSetRange([0x61, 0x61])), + () => assertEq(toRange('a'), one(0x61)), + () => assertEq(toRange('\0'), one(0)), + ], dfa: () => { const dfa = buildDfa() const entries = Object.entries(dfa) diff --git a/fjs/fsm/todo/torange-ascii-range.md b/fjs/fsm/todo/torange-ascii-range.md deleted file mode 100644 index ea1870c5c..000000000 --- a/fjs/fsm/todo/torange-ascii-range.md +++ /dev/null @@ -1,42 +0,0 @@ -## `toRange` re-implements `ascii.range` and crashes on one character - -**Priority:** P3 -**Status:** open - -### Problem - -```js -export const toRange = s => { - const [b, e] = toArray(stringToList(s)) - return range([b, e]) -} -``` - -`fjs/text/ascii/module.f.mjs:20-25` already owns "two-character string → -inclusive `Range`", including the one-character case. `fsm.toRange` -(`module.f.mjs:32-36`) is `compose(asciiRange)(byteSetRange)` written out by -hand — and the duplicate is worse than the original: - -``` -toRange('a') → RangeError: The number NaN cannot be converted to a BigInt -``` - -because `e` destructures to `undefined` and `byte_set.range` computes -`one(undefined - b + 1)`. `fjs/fsc/module.f.mjs:66` shows the correct -composition (`fn(asciiRange).map(codePointRange)`). - -`toUnion` (`:39-45`) — "byte set from the characters of a string" — is -likewise generic byte-set vocabulary, the same shape as `fjs/bnf`'s `set(s)` -for its own alphabet. Both exports have no consumer outside -`fjs/fsm/proof.f.mjs`. - -### Proposal - -`export const toRange = compose(asciiRange)(byteSetRange)` (or drop the -export if the proof stays the only caller); move `toUnion` to -`fjs/types/byte_set` next to `one`/`set`. - -### Tasks - -- [ ] Rebuild `toRange` on `ascii.range`; cover the one-character case -- [ ] Move `toUnion` to `byte_set` (or inline it into the proof) From 67d8af759eddb47d4bda030e034f580f0b451f54 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 01:33:44 +0000 Subject: [PATCH 2/2] changelog: entry for the fsm toRange fix Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs --- changelog/unreleased/1575.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/unreleased/1575.md diff --git a/changelog/unreleased/1575.md b/changelog/unreleased/1575.md new file mode 100644 index 000000000..c1ce77e7e --- /dev/null +++ b/changelog/unreleased/1575.md @@ -0,0 +1,3 @@ +- **BREAKING CHANGES:** `fsm`: `toRange` no longer throws on a one-character + argument — it is the singleton range, via `text/ascii`'s `range`. `toUnion` + is no longer exported; it was used only by the module's own proof