Skip to content

Remove unreachable fallback branch in UTF-16 decode step - #1540

Merged
sergey-shandar merged 5 commits into
mainfrom
claude/utf16-coverage-improvement
Aug 14, 2026
Merged

Remove unreachable fallback branch in UTF-16 decode step#1540
sergey-shandar merged 5 commits into
mainfrom
claude/utf16-coverage-improvement

Conversation

@sergey-shandar

Copy link
Copy Markdown
Contributor

Summary

  • fjs/text/utf16/module.f.mjs was at 96.55% branch coverage: utf16ByteToCodePointOp's trailing fallback (return [[state | errorMask, word | errorMask], null]) could never run.
  • isLowSurrogate, isBmpCodePoint, and isHighSurrogate partition the full u16 range (0x00000xFFFF) with no gap. By the point the fallback is reached, isLowSurrogate has already been ruled out, so a non-BMP word is always a high surrogate — the isHighSurrogate check and its fallback were redundant.
  • Per 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.mjs now reaches 100% line/branch/function coverage.

Test plan

  • npx tsc --noEmit
  • node --test --experimental-test-coverage --test-coverage-include='fjs/text/utf16/module.f.mjs' fjs/emergent_testing/all.test.mjs → 100.00% line/branch/func
  • node ./fjs/module.mjs t → 2551 pass, 0 fail

🤖 Generated with Claude Code


Generated by Claude Code

claude added 2 commits August 13, 2026 22:52
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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 13, 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 4e2b673 Commit Preview URL

Branch Preview URL
Aug 14 2026, 02:11 AM

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Comment on lines +194 to +198
// `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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deleted in fe050dd.


Generated by Claude Code

Comment on lines +194 to +198
// `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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread fjs/text/utf16/module.f.mjs Outdated
* @type {(i: U16) => boolean}
*/
const u16 = contains(0x0000, 0xFFFF)
const u16 = i => Number.isInteger(i) && contains(0x0000, 0xFFFF)(i)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

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 0x00000xFFFF; 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: utf16ByteToCodePointOp drops its trailing fallback arm — after ruling out a low
surrogate, a non-BMP word is always a high surrogate, so the isHighSurrogate recheck and its
errorMask fallback 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: u16 now 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 in 0x00000xFFFF. That closes the only path into
utf16ByteToCodePointOp's trailing fallback arm, which is removed along with its
isHighSurrogate recheck.

The rest of the verification battery, on top of the sweeps above:

  • npx tsc --noEmit — exit 0.
  • npm run prepack from a cleaned tree — exit 0 (both passes).
  • npm test — 2553 pass / 0 fail, vs 2551 on origin/main; +2 is exactly the two new proof cases.
  • Public surface, bin/extract.mjs + bin/consts.mjs after prepack in both trees — byte-identical
    to main. No new exports, no widening to any.
  • bin/linkcheck.mjs — broken-link sets identical to main (137 each). Deleting
    todo/unreachable-pending-state-fallthrough.md strands 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.

Copy link
Copy Markdown
Contributor Author

Fixed in 4e2b673 — reworded the CHANGELOG entry to your suggested shape, attributing the observable behavior change to the u16 tightening rather than the branch removal. Thanks for the precise breakdown of what was actually reachable on main.


Generated by Claude Code

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

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:

u16 now 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 into utf16ByteToCodePointOp's trailing fallback arm, which is removed along with its isHighSurrogate recheck

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 prepack in both trees, then extract.mjs (46 326 bytes on main) and consts.mjs (64 315 bytes on main) — both diffs empty. No export added, removed, retyped, or widened to any; no new unprefixed public export type. u16 is 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-range 0x10000/-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 u16 now admitting only integers in 0x00000xFFFF, the count of integers in that range classified by none of isLowSurrogate/isBmpCodePoint/isHighSurrogate is 0 — over all 65 536 of them, not a sample. Negative control: re-running the same sweep against a deliberately holed predicate (isBmpCodePoint minus 0x41) reports 1, so the check can see a gap when one exists. Since isLowSurrogate is taken above, a non-BMP word at that point is always a high surrogate; the deleted return had no preimage.
  • npx tsc --noEmit — exit 0.
  • npm run prepack from a clean tree — exit 0, both trees.
  • npm test — 2553 pass / 0 fail, vs 2551 on e7e1881a. The +2 are exactly the two new toCodePointList proof cases.
  • npm run covfjs/text/utf16/module.f.mjs at 100.00 / 100.00 / 100.00 (lines / branches / functions), which is the acceptance criterion the deleted unreachable-pending-state-fallthrough.md set 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 to unreachable-pending-state-fallthrough anywhere 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.

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