Skip to content

refactor(hmr): unify the Swift<->JS scalar crossing behind one JSScalar type#150

Merged
zzal merged 1 commit into
mainfrom
feat/jsscalar-unification
Jul 6, 2026
Merged

refactor(hmr): unify the Swift<->JS scalar crossing behind one JSScalar type#150
zzal merged 1 commit into
mainfrom
feat/jsscalar-unification

Conversation

@zzal

@zzal zzal commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Closes Part I Wave 3 #4 of the arch+DX audit. The "which JS primitive is this" rule was re-derived in three JavaScriptKit-only paths — encodeStateMapToJS, HMRBridge.decodeStateMap, and JSAdapter's PatchPayload.Field marshalling — alongside a parallel PropertyValue taxonomy. The subtle rules (Bool-before-Int, HMRNilSentinel↔null, integral-number→Int) lived where no unit test could reach them — the exact gap the "Blocker 2/3" HMR coercion bugs shipped through.

Approach — unify and close the coverage gap

A closed core JSScalar enum (string/int/double/bool/null), split so the fragile classification becomes host-testable:

  • core JSScalar(stateValue:) / .stateValue — the @State-snapshot side, carrying the Bool-before-Int ordering (Bool bridges to NSNumber, so as? Int-first would misclassify it) and HMRNilSentinel↔null;
  • PropertyValue.jsScalar — DOM properties map to the same taxonomy;
  • a thin, wasm-only JSScalar(jsValue:) / .jsValue crossing in SwiflowDOM (the integral-number→Int rule), keeping Swiflow core free of JavaScriptKit.

The three crossings now route through it — four hand-rolled primitive-mappings collapse to one. _hmrCoerce is left untouched: on inspection it's a different concern (decoded-IntDouble-@State target coercion), not a JS crossing — noted in code.

The win beyond de-dup: the previously-untested, previously-buggy classification is now unit-tested, so a coercion slip is a red test rather than silent HMR-state corruption found by hand.

Test plan

  • 6 new host tests pinning the extracted Any↔JSScalar coercion: Bool-before-Int, HMRNilSentinel↔null, unsupported→nil, stateValue round-trip, PropertyValue.jsScalar.
  • Behavior-preserving: every existing HMR round-trip test still passes — they simulate decodeStateMap's output, which JSScalar reproduces byte-for-byte.
  • Full Swift monorepo suite 1367/274 (+6).
  • HelloWorld wasm build + link clean (the crossing compiles; swift package cleaned the example first for the new JSScalar.swift).
  • Counter Playwright suite 11/11 — real-browser property marshalling (TextField value / Checkbox) through JSScalar.jsValue.

Note: the wasm JSValueJSScalar crossing itself is still build-only-verified (no WASM test target exists) — but it is now thin and centralized, and the fragile Swift-side rules moved into the host-tested core, a net reduction in untested surface.

🤖 Generated with Claude Code

…ar type

Closes Part I Wave 3 #4 of the arch+DX audit. The "which JS primitive is this"
rule was re-derived in three JavaScriptKit-only paths (encodeStateMapToJS,
HMRBridge.decodeStateMap, JSAdapter's PatchPayload.Field marshalling) plus the
parallel PropertyValue taxonomy — with the SUBTLE rules (Bool-before-Int,
HMRNilSentinel<->null, integral-number) living where no unit test could reach
them. That's the exact gap the "Blocker 2/3" HMR coercion bugs shipped through.

Introduces a closed core `JSScalar` enum (string/int/double/bool/null) split so
the fragile classification is HOST-TESTABLE:
- core `JSScalar(stateValue:)` / `.stateValue` carry the @State-snapshot rules,
  incl. Bool-before-Int and HMRNilSentinel<->null;
- `PropertyValue.jsScalar` maps DOM properties to the same taxonomy;
- a thin, wasm-only `JSScalar(jsValue:)` / `.jsValue` crossing (SwiflowDOM) holds
  the integral-number->Int rule, keeping Swiflow core free of JavaScriptKit.

The three crossings now route through it (four hand-rolled mappings -> one).
`_hmrCoerce` is left as-is: on inspection it's a different concern (decoded-Int
-> Double-@State target coercion), not a JS crossing — noted in code.

Behavior-preserving: every existing HMR round-trip test still passes (they
simulate decodeStateMap's output, which JSScalar reproduces exactly). Adds 6
host tests pinning the previously-untested Any<->scalar coercion, so a
classification slip is now a unit-test failure, not silent HMR corruption.

Verified: host suite 1367/274, HelloWorld wasm build clean, counter e2e 11/11
(real-browser property marshalling via JSScalar.jsValue).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

📦 Bundle size

Artifact Baseline This PR Δ
App.wasm 5.06 MB 5.15 MB +1.90%
App.wasm (gzip) 1.80 MB 1.84 MB +2.21%
JS runtime 54.5 KB 54.5 KB +0.00%
JS runtime (gzip) 11.3 KB 11.3 KB +0.00%
Total (gzip) 1.81 MB 1.85 MB +2.19%

✅ Within budget (≤5% growth allowed).

Baseline: Swift 6.3, WASM SDK 6.3-RELEASE, measured 2026-06-18.

@zzal zzal merged commit e22c2d4 into main Jul 6, 2026
6 checks passed
@zzal zzal deleted the feat/jsscalar-unification branch July 6, 2026 17:21
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.

1 participant