Skip to content

Remove unreachable throw in djs serializer's constSerialize - #1544

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

Remove unreachable throw in djs serializer's constSerialize#1544
sergey-shandar merged 5 commits into
mainfrom
claude/djs-serializer-coverage-improvement

Conversation

@sergey-shandar

Copy link
Copy Markdown
Contributor

Summary

  • fjs/djs/serializer/module.f.mjs was at 99.03% line / 98.72% branch coverage: stringify's inner constSerialize threw 'unexpected behavior' when refs.get(entry) was undefined, but that can never happen through the public stringify.
  • consts (the list constSerialize iterates) is built by getConstants, which only adds a value via checkSelf when shared(djs) !== undefined — and shared itself only returns non-undefined when refs.get(v) already has an entry. So every value in consts is guaranteed to already have a refs entry by construction.
  • Per AGENTS.md §3.2, the branch is removed rather than covered with a direct-invocation test — replaced with a type assertion and a comment documenting the invariant.
  • fjs/djs/serializer/module.f.mjs now reaches 100% line/branch/function coverage.

Test plan

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

🤖 Generated with Claude Code


Generated by Claude Code

consts only ever holds values getConstants found shared for — i.e.
values that already have an entry in refs — so refs.get(entry) inside
constSerialize can never be undefined through the public stringify.
Replace the defensive throw with a type assertion documenting the
invariant, per AGENTS.md's guidance to restructure genuinely
unreachable code away rather than leave it uncovered.

fjs/djs/serializer/module.f.mjs reaches 100% line/branch/function
coverage.
@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 47417f9 Commit Preview URL

Branch Preview URL
Aug 14 2026, 07:46 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: 161a548d56

ℹ️ 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/djs/serializer/module.f.mjs Outdated
if (refCounter === undefined) {
throw 'unexpected behavior'
}
const refCounter = /** @type {_RefCounter} */ (refs.get(entry))

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 Carry the counter instead of asserting it exists

If getConstants is later changed so an emitted entry lacks a corresponding refs value, this inline @type cast suppresses the compiler warning and makes refCounter[0] fail at runtime. Preserve the invariant in the types instead—for example, have getConstants carry each _RefCounter alongside its entry—rather than overriding the inferred _RefCounter | undefined; repository guidance explicitly applies the avoid-assertions rule to inline JSDoc casts.

AGENTS.md reference: AGENTS.md:L678-L685

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 4ba4111 by using assertNotNullish (a runtime-checked narrowing helper already used elsewhere in the codebase) instead of an unchecked @type cast — if the invariant ever breaks, this throws immediately rather than silently reading undefined[0].

I considered the "carry the counter alongside its entry" alternative too, but it would change getConstants's return type from List<Unknown> to something pairing each entry with its _RefCounter, which is a larger, more invasive change than this coverage-focused PR warrants — assertNotNullish gets the same safety (loud failure, not silent) with a much smaller diff.


Generated by Claude Code

Comment thread fjs/djs/serializer/module.f.mjs Outdated
if (refCounter === undefined) {
throw 'unexpected behavior'
}
const refCounter = /** @type {_RefCounter} */ (refs.get(entry))

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.

use assert instead of type casting

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 4ba4111 — switched to assertNotNullish(refs.get(entry)), which narrows via a runtime check instead of trusting an unchecked cast.


Generated by Claude Code

sergey-shandar and others added 2 commits August 14, 2026 00:39
Address review feedback: assertNotNullish narrows refCounter's type
via a runtime check instead of trusting an unchecked cast, while
still failing loudly (not silently) if the invariant it documents
ever breaks.
Prompted by review discussion on this PR: an inline @type cast
documents an invariant but doesn't check it, so a broken assumption
fails silently instead of at the point that assumed it.
assert/assertNotNullish narrow the same way while also verifying the
claim at runtime.

Copy link
Copy Markdown
Contributor Author

Added in 47417f9: a new paragraph in AGENTS.md's "Avoid as type assertions" section recommending assert/assertNotNullish over a cast when narrowing an invariant, using this PR's refCounter fix as the worked example. No CHANGELOG entry needed (docs-only commit, per §8.3).


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.

Approving. Baseline for every comparison below is origin/main at 6684bef2.

What the change actually is

Worth stating plainly, because the title and the CHANGELOG both read as a deletion: the
defensive check is not removed. throw 'unexpected behavior' becomes
assertNotNullish(refs.get(entry)), which still throws on a miss — only the thrown value
changes ('unexpected behavior''assertion failed'). So even in the worst case where the
invariant were wrong, this is a message change on a throw path, not a newly-silent wrong value.
That is a materially lower-risk shape than a genuine dead-arm deletion, and it matches the
new AGENTS.md guidance the same PR adds.

Is the lookup really unreachable?

Statically it is a local, one-hop invariant: getConstants pushes djs onto consts only
under shared(djs) !== undefined, and sharedRef is refs.get(v) with a rc[1] > 1 filter —
same refs binding, never reassigned between the two uses. So refs.get(entry) in
constSerialize cannot miss.

Empirically, I instrumented the branch with a distinct throw 'PROBE_HIT' and drove the public
entry point stringify over a deterministic random DJS domain (all primitives incl. NaN,
-0, ±Infinity, '', bigints incl. a 30-digit one, null/undefined, arrays and objects to
depth 4, with a reuse pool so subvalues are deliberately shared — sharing is the only thing that
produces consts at all), under both sort and identity for the sort parameter:

PR head 47417f90:  runs 400000, runs that emitted >=1 `const cN` 88096, PROBE_HIT 0, other errors 0

Negative control (relaxed getConstants' checkSelf guard to !state.added.has(djs), so
non-shared values — numbers, booleans, null, which countRefs never adds to refs — reach
constSerialize): runs 240432, PROBE_HIT 159568. The harness detects a hit.

Same probe against origin/main

Per the lesson from #1540 — where the removed arm was dead at the PR head but live on main,
killed by a separate unmentioned tightening — I ran the identical probe against 6684bef2 with
main's own throw re-labelled 'PROBE_HIT':

origin/main 6684bef2: runs 400000, with-consts 88096, PROBE_HIT 0, other errors 0

Dead on main too, and the module diff is the only change. Nothing else in this PR tightens the
input domain, so #1540's pattern does not apply here.

Behaviour neutrality

Same 400k-call sweep, SHA-256 over every emitted string (NUL-separated), both trees unmodified:

PR head 47417f90:      80d2a25b05295dcecb9ba1eac1bcb91caf4637d6817648babc7ced5c854afb5a
origin/main 6684bef2:  80d2a25b05295dcecb9ba1eac1bcb91caf4637d6817648babc7ced5c854afb5a

Identical. No divergence to classify.

Battery

  • npx tsc --noEmit — exit 0. The assertNotNullish return type narrows well enough that
    refCounter[0] still type-checks with no cast.
  • npm run prepack from a freshly cleaned tree — exit 0 (both passes).
  • npm testpass: 2646, fail: 0 on the PR head; pass: 2646, fail: 0 on origin/main.
    Exact match.
  • Dual-axis public surface (bin/extract.mjs / bin/consts.mjs over the emitted .d.mts,
    prepack run in both trees) — byte-identical on both axes. Expected: the edit is inside a
    function body. No unprefixed type additions to weigh against §6.2, nothing widened to any.
  • bin/linkcheck.mjs — broken-link sets identical (154 = 154, diff empty). The new
    AGENTS.md link to ./fjs/asserts/module.f.mjs resolves.
  • Rust gates skipped: nanvm-lib/ untouched.

Conventions

  • §8.3: code change, one entry, links the PR only. AGENTS.md is docs and correctly gets no
    entry of its own. Released sections untouched.
  • §8.4: no **BREAKING CHANGES:** needed. This sits with #1524 (representation-only, correctly
    unprefixed) rather than #1520 (specifier-level break): the emitted surface is provably
    identical, and the only observable delta is the thrown value on a path proven unreachable on
    both main and the PR head.
  • §6.2 / §4: no new types, no header changes.

The AGENTS.md section reads well and the assert-over-cast rationale is the right one — a cast
is erased, an assertion fails where the invariant actually broke.

One nit (non-blocking)

The CHANGELOG entry says constSerialize "drops a defensive throw". It doesn't drop it; it
swaps throw 'unexpected behavior' for assertNotNullish, which is still a throw. Something
like "replaces a hand-rolled undefined check with assertNotNullish" would describe the
diff more exactly — and would make the entry an example of the convention the PR is
simultaneously writing into AGENTS.md, which seems like the more useful thing for it to say.
Not worth a re-push on its own.

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