Remove unreachable fallback branch in UTF-16 decode step - #1540
Conversation
isLowSurrogate, isBmpCodePoint, and isHighSurrogate partition the full u16 range with no gap; after isLowSurrogate is ruled out, a non-BMP word is always a high surrogate, so the trailing fallback in utf16ByteToCodePointOp could never run. Drop it instead of testing around it, per AGENTS.md's guidance to restructure genuinely unreachable code away rather than leave it uncovered. fjs/text/utf16/module.f.mjs reaches 100% line/branch/function coverage.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | 4e2b673 | Commit Preview URL Branch Preview URL |
Aug 14 2026, 02:11 AM |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fefab25d23
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isBmpCodePoint(word)) { return [[state | errorMask, word], null] } | ||
| if (isHighSurrogate(word)) { return [[state | errorMask], word] } | ||
| return [[state | errorMask, word | errorMask], null] | ||
| return [[state | errorMask], word] |
There was a problem hiding this comment.
Preserve rejection of fractional surrogate-gap values
U16 is only an alias for number, and u16 checks only the inclusive numeric range, so values between the surrogate ranges are reachable. For example, decoding [0xd800, 0xdbff + 0.5, 0xdc00] now treats the fractional middle value as a pending high surrogate and emits 0x10fc00 as if it were a valid pair; the removed fallback previously emitted masked errors for both invalid units. Retain a fallback or reject non-integers before unconditionally treating this branch as a high surrogate.
AGENTS.md reference: AGENTS.md:L401-L403
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in fe050dd — I verified the gap is real (contains is a plain <=/>= check, so e.g. 56319.5 matched none of isHighSurrogate/isLowSurrogate/isBmpCodePoint). Tightened u16 with Number.isInteger so the three predicates' exhaustive-partition guarantee actually holds for every input the function can receive, and added proof cases for a fresh and a pending-state fractional word.
Generated by Claude Code
| // `isLowSurrogate`, `isBmpCodePoint`, and `isHighSurrogate` partition the | ||
| // full `u16` range with no gap, and `isLowSurrogate` was already ruled out | ||
| // above, so a non-BMP `word` here is always a high surrogate. | ||
| if (isBmpCodePoint(word)) { return [[state | errorMask, word], null] } | ||
| if (isHighSurrogate(word)) { return [[state | errorMask], word] } | ||
| return [[state | errorMask, word | errorMask], null] | ||
| return [[state | errorMask], word] |
There was a problem hiding this comment.
Delete the resolved UTF-16 todo
This change implements fjs/text/utf16/todo/unreachable-pending-state-fallthrough.md, but that issue file remains open in the commit, so the repository's issue tracker continues to advertise already-completed work. Delete the resolved todo as part of this change.
AGENTS.md reference: AGENTS.md:L1170-L1176
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
| // `isLowSurrogate`, `isBmpCodePoint`, and `isHighSurrogate` partition the | ||
| // full `u16` range with no gap, and `isLowSurrogate` was already ruled out | ||
| // above, so a non-BMP `word` here is always a high surrogate. | ||
| if (isBmpCodePoint(word)) { return [[state | errorMask, word], null] } | ||
| if (isHighSurrogate(word)) { return [[state | errorMask], word] } | ||
| return [[state | errorMask, word | errorMask], null] | ||
| return [[state | errorMask], word] |
There was a problem hiding this comment.
Add the required changelog entry
This commit changes authored FunctionalScript code but leaves CHANGELOG.md unchanged, so the release notes will omit the UTF-16 decoder change. Add a short entry at the top of Unreleased with the real PR link as required for code changes.
AGENTS.md reference: AGENTS.md:L1194-L1203
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already added in b7ae07e, before this review ran — see CHANGELOG.md's ## Unreleased section, text/utf16 entry linking #1540.
Generated by Claude Code
u16 gated only on numeric range, but the isBmpCodePoint/isHighSurrogate/ isLowSurrogate predicates only partition the *integers* in 0x0000-0xFFFF with no gap. A fractional word inside that range (e.g. 56319.5) fell into none of them, so removing the dead fallback made such a word get misclassified as a high surrogate instead of rejected. Tighten u16 with Number.isInteger so the removed branch stays genuinely unreachable for every input the function can receive, and add proof cases for a fresh and a pending-state fractional word. Also delete todo/unreachable-pending-state-fallthrough.md, which this change resolves.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe050dde98
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * @type {(i: U16) => boolean} | ||
| */ | ||
| const u16 = contains(0x0000, 0xFFFF) | ||
| const u16 = i => Number.isInteger(i) && contains(0x0000, 0xFFFF)(i) |
There was a problem hiding this comment.
Hoist invariant predicates out of the decoder hot path
For every code unit in a UTF-16 list, u16 now reevaluates Number.isInteger and calls the curried contains(0x0000, 0xFFFF) factory, creating a new range predicate instead of reusing the single predicate the previous implementation constructed. This adds avoidable per-unit work to long text decoding; bind the integer function and range predicate once at module scope, then have u16 invoke those bindings.
AGENTS.md reference: AGENTS.md:L982-L986
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in b98491f: hoisted contains(0x0000, 0xFFFF) to a module-level isInU16Range binding, matching the other range predicates in this file; u16 now just calls Number.isInteger(i) && isInU16Range(i).
Generated by Claude Code
contains(0x0000, 0xFFFF) built a fresh closure on every u16 call; bind it once at module scope like the rest of the file's range predicates.
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Reviewed at b98491f3 against origin/main e7e1881a.
The code change is correct, and I proved the unreachability rather than taking it on trust. One
thing to fix: the CHANGELOG entry describes the wrong half of the change.
The branch is dead — but only because this PR made it dead
I instrumented the removed arm with a throw and drove toCodePointList over its full domain:
all 65,536 code units in every state position (fresh, pending high surrogate at both ends of the
high range, after a BMP word, after a lone low surrogate, after a completed pair), each with the
empty suffix and with [0xDC00] / [0x0041] / [0xD800] appended, plus empty input, truncated
pairs, valid-pair-then-EOF, and out-of-domain words (-1, 0x10000, NaN, ±Infinity, 1e21,
and fractional values straddling each range boundary). 1,835,456 sequences.
| tree | instrumented arm | hits |
|---|---|---|
| this PR | if (isHighSurrogate(word)) {…} restored, throw on fall-through |
0 / 1,835,456 |
| negative control | same, guard inverted (!isHighSurrogate) |
13,344 — harness detects hits |
origin/main |
throw in place of the fall-through arm |
24 |
So on the merge base the arm is not dead. It fires on non-integer words in a pending-surrogate
state — e.g. toCodePointList([0xD800, 55295.5]), [0xD800, 56319.5], [0xD800, 57343.5]. The
three predicates partition only the integers in 0x0000–0xFFFF; contains is b <= i && i <= e,
so 56319.5 slips past u16, past isLowSurrogate, past isBmpCodePoint and past
isHighSurrogate, landing in the fall-through. Nothing to do with #1537's shared EOF flush — that
change did not touch this classifier's reachability.
What makes the arm dead is the other edit in this PR: u16 = i => Number.isInteger(i) && isInU16Range(i).
That is the right fix, the reasoning in the new JSDoc is exactly correct, and the two new proof
cases pin it.
Deletion changes nothing on the integer domain
Same sweep, old toCodePointList vs new, restricted to integer words: 1,835,036 sequences, 0
differences. Over the full sweep including non-integers: 196 of 1,835,456 differ, all of them
non-integer inputs, and all of them improvements — main returns toCodePointList([0.5]) === [0.5],
i.e. it emits the fractional value as a code point; this PR returns [0xFFFFFFFF]. Likewise
[55295.5] was [-2147428353] (a flagged-but-fractional code point) and is now the invalid
sentinel.
§8.3 — the entry describes the removal, not the fix
text/utf16:utf16ByteToCodePointOpdrops its trailing fallback arm — after ruling out a low
surrogate, a non-BMP word is always a high surrogate, so theisHighSurrogaterecheck and its
errorMaskfallback were dead code
They were not dead code on main; they became dead code in this commit. And the branch removal is
the part with no observable effect, while the u16 tightening — the part the entry doesn't
mention — is the only user-visible change here (196 input sequences change output). As written the
entry tells a reader "nothing changed for you", which is wrong for anyone feeding non-integer
U16 values.
Suggested shape:
text/utf16:u16now rejects non-integers, so a fractional word is reported invalid
(0xFFFFFFFF) instead of being misclassified by the surrogate/BMP range checks — which only
partition the integers in0x0000–0xFFFF. That closes the only path into
utf16ByteToCodePointOp's trailing fallback arm, which is removed along with its
isHighSurrogaterecheck.
The rest of the verification battery, on top of the sweeps above:
npx tsc --noEmit— exit 0.npm run prepackfrom a cleaned tree — exit 0 (both passes).npm test— 2553 pass / 0 fail, vs 2551 onorigin/main; +2 is exactly the two new proof cases.- Public surface,
bin/extract.mjs+bin/consts.mjsafterprepackin both trees — byte-identical
tomain. No new exports, no widening toany. bin/linkcheck.mjs— broken-link sets identical tomain(137 each). Deleting
todo/unreachable-pending-state-fallthrough.mdstrands nothing; nothing in the tree references it.
Not blocking the code — just the changelog wording.
The isHighSurrogate branch was still reachable on main (via a fractional word in a pending-surrogate state); it only became dead once u16 was tightened to reject non-integers. The entry described the branch removal as the change when the observable difference is entirely from the u16 fix.
|
Fixed in 4e2b673 — reworded the CHANGELOG entry to your suggested shape, attributing the observable behavior change to the Generated by Claude Code |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Re-reviewed at 4e2b6738, baseline origin/main = e7e1881a (unchanged since the last round). The open finding from b98491f3 is resolved — approving.
The previous finding is fixed
Last round the entry read "…the isHighSurrogate recheck and its errorMask fallback were dead code", which documented the no-op half of the change and said nothing about the u16 tightening that is the actual user-visible half. The only diff between b98491f3 and 4e2b6738 is CHANGELOG.md, and it now leads with the real change:
u16now rejects non-integers, so a fractional word is reported invalid (0xFFFFFFFF) instead of being misclassified by the surrogate/BMP range checks … That closes the only path intoutf16ByteToCodePointOp's trailing fallback arm, which is removed along with itsisHighSurrogaterecheck
That is the right causal order: the u16 change is the change, the deletion is its consequence. Nothing left to say here.
§8.4: no **BREAKING CHANGES:** prefix, and "fix" is the right characterisation
Verified rather than assumed, since the assessment turns on it:
- Public surface is untouched.
npm run prepackin both trees, thenextract.mjs(46 326 bytes on main) andconsts.mjs(64 315 bytes on main) — both diffs empty. No export added, removed, retyped, or widened toany; no new unprefixed publicexport type.u16is module-private and stays that way. - No valid input changes behaviour. 400 000 driven sequences (length 1–5) over integer boundary words (
0,0x41,0xD7FF,0xD800,0xDBFF,0xDC00,0xDFFF,0xE000,0xFFFF, plus out-of-range0x10000/-1), main vs this head: 0 diffs. - The inputs that do change were already broken. 200 000 mixed sequences seeded with fractional words: 146 884 diffs — every one of them main producing something that is not a code point.
toCodePointList([0.5])returned[0.5]on main and returns[4294967295]here;toCodePointList([55296, 56319.5])returned[-2147428352, -2147427329]and now returns[4294967295, -2147428352].
So the only observable difference is on inputs the declared U16 contract already excludes, and on those the function moves from violating its own output contract (emitting 0.5 as a code point) to emitting the documented invalid sentinel. SemVer's break signal is about the API contract, not about every observable value, and no contract-conforming caller can be broken by this.
On the repo's own precedent this is the #1524 case, not the #1520 case. #1520's prefix records a specifier-level break — files that consumers import by path stopped existing. #1524 (decToBin no longer returning a 54-bit mantissa) changed what comes out of a function, kept the API shape, and correctly carried no prefix. #1540 is the same shape as #1524, and arguably a weaker claim on the prefix: #1524 changed the representation of results for in-contract inputs, whereas #1540 leaves in-contract inputs bit-identical and only corrects out-of-contract ones. "Reported invalid … instead of being misclassified" reads as a fix because it is one. No prefix.
Re-verified at this head
The code is byte-identical to b98491f3 (diff is CHANGELOG.md only), but the reachability argument is the whole basis of the change, so I re-derived it rather than carrying it forward — and this time exhaustively rather than by sampling:
- The removed arm is unreachable, by exhaustion. With
u16now admitting only integers in0x0000–0xFFFF, the count of integers in that range classified by none ofisLowSurrogate/isBmpCodePoint/isHighSurrogateis 0 — over all 65 536 of them, not a sample. Negative control: re-running the same sweep against a deliberately holed predicate (isBmpCodePointminus0x41) reports 1, so the check can see a gap when one exists. SinceisLowSurrogateis taken above, a non-BMPwordat that point is always a high surrogate; the deletedreturnhad no preimage. npx tsc --noEmit— exit 0.npm run prepackfrom a clean tree — exit 0, both trees.npm test— 2553 pass / 0 fail, vs 2551 one7e1881a. The +2 are exactly the two newtoCodePointListproof cases.npm run cov—fjs/text/utf16/module.f.mjsat 100.00 / 100.00 / 100.00 (lines / branches / functions), which is the acceptance criterion the deletedunreachable-pending-state-fallthrough.mdset for itself. Whole tree 99.94 / 98.37 / 99.78.linkcheck.mjs— 137 broken relative links, byte-identical sets on both trees; deleting the todo stranded nothing (no remaining reference tounreachable-pending-state-fallthroughanywhere in the tree).- Rust gates not run:
nanvm-lib/untouched.
The two new proof cases are the right ones — they pin both the bare fractional word and the fractional-word-during-pending-surrogate interaction, which is the case where the old code produced its worst output.
Summary
fjs/text/utf16/module.f.mjswas at 96.55% branch coverage:utf16ByteToCodePointOp's trailing fallback (return [[state | errorMask, word | errorMask], null]) could never run.isLowSurrogate,isBmpCodePoint, andisHighSurrogatepartition the fullu16range (0x0000–0xFFFF) with no gap. By the point the fallback is reached,isLowSurrogatehas already been ruled out, so a non-BMPwordis always a high surrogate — theisHighSurrogatecheck and its fallback were redundant.AGENTS.md§3.2 ("If a line or branch genuinely cannot be reached, restructure the code so it isn't there rather than leaving it uncovered"), the branch is removed rather than covered with a direct-invocation test.fjs/text/utf16/module.f.mjsnow reaches 100% line/branch/function coverage.Test plan
npx tsc --noEmitnode --test --experimental-test-coverage --test-coverage-include='fjs/text/utf16/module.f.mjs' fjs/emergent_testing/all.test.mjs→ 100.00% line/branch/funcnode ./fjs/module.mjs t→ 2551 pass, 0 fail🤖 Generated with Claude Code
Generated by Claude Code