Skip to content

Real-usable wave 1: runtime correctness, node-like CLI, TypeScript, test262 gate, latest deps - #2

Merged
wuchendi merged 109 commits into
mainfrom
feat/real-usable-wave1
Oct 19, 2026
Merged

wuchendi merged 109 commits into
mainfrom
feat/real-usable-wave1

Conversation

@wuchendi

Copy link
Copy Markdown
Owner

Summary

Moves jsc from a teaching compiler toward a real-usable JavaScript/TypeScript to WebAssembly compiler, with test262 as the conformance metric.

Runtime correctness

  • Guest memory grows on demand (512 MiB cap); exhaustion reports Uncaught RangeError: out of memory instead of a wasm trap backtrace.
  • Property read, write and call on null/undefined throw a catchable TypeError.
  • NaN is canonicalized at every rebox; equality kernels and includes/Map/Set use SameValueZero correctly.
  • Top-level functions that capture top-level bindings work from any call site (module cells) instead of miscompiling, trapping, or being rejected.
  • Calls no longer require exact arity; missing arguments are undefined.
  • Arithmetic, unary, update and relational operators apply ToNumber to strings.
  • C-style for (let/const ...) loops create a fresh binding per iteration for captured variables.
  • Writing a property an object does not have, or growing an array by index, throws TypeError instead of silently doing nothing (dynamic properties come with the object-model rework).

Frontend

  • swc bumped to the latest majors (common 26, ast 29, parser 46).
  • Recoverable parse errors are reported as compile errors instead of being ignored.
  • .ts, .mts, .cts files compile via the swc TypeScript strip pass; .tsx is rejected; -e/-p stay JavaScript.

CLI and browser package

  • Node-like CLI: jsc FILE and jsc -e SRC print console output only, jsc -p SRC prints the completion value, jsc build FILE -o OUT emits wasm. Exit codes: 2 compile error, 1 uncaught exception, 0 success. --emit is removed.
  • The browser shim implements all host imports (__promise, __then, __now were missing, so every module failed to instantiate).
  • The web workflow runs on push and pull requests; scripts/ensure-wasm.sh rebuilds a stale artifact.

Conformance

  • New crates/jsc-test262 runner: pinned test262 checkout, jsc-compatible harness shims, feature/flag filtering (strict mode only), per-directory report, baseline.txt regression gate, history.tsv, and a test262 CI job.
  • Pass count: 4704 to 5195 of 22992 eligible tests (22.6%).

Dependencies and toolchain

  • Rust 1.99.1 (MSRV 1.99), wasmtime 49.0.3 (clears 16 RUSTSEC advisories), wasm-encoder/wasmparser 0.262, all other crates and JS packages at their latest releases, GitHub Actions on their latest majors. cargo deny check passes.

Docs

  • README and DESIGN describe the current product; development-history tags removed from code comments, test names and diagnostics.

Known limitations (next)

  • Objects are still fixed-shape: dynamic properties and array growth by index are not implemented yet (they throw).
  • Property writes on primitive receivers need a pointer guard / ToObject.
  • push copies the array (quadratic cost); push on captured arrays is rejected.
  • 1 + null, "abc".foo, user valueOf/toString in ToPrimitive, and TS enum/namespace are not handled yet.

Verification

On the final tree:

  • cargo fmt --all --check, cargo clippy --all-targets --workspace, cargo nextest run --workspace (817 passed), cargo test --doc --workspace, typos, cargo shear, cargo deny check
  • bun run lint, @cdlab/jsc and @cdlab/jsc-web typecheck and build, @cdlab/jsc tests (18 passed) against a freshly built wasm package
  • cargo run --release -p jsc-test262 -- --check: 5195 / 22992 eligible, 0 regressions

…after main

Every emitted module imports env.__promise/__then/__now, but the shim only
provided print/__convert/__method, so instantiation failed with a LinkError
for every program. Port the host promise engine from run/promise.rs (promise
blocks in guest memory, FIFO microtask queue, reactions, re-entry through
__invoke1/__async_step, exception-global handling), back __now with
Date.now(), drain after main unless __exc_pending is set, and render Promise
and Date values like format_value.
Frontmatter parsing, eligibility filtering, program assembly with the
assert adapter rewrite, outcome classification, baseline diffing, test
discovery and report aggregation, each with unit tests.
- jsc FILE / -e SRC run and print only console output
- -p/--print SRC prints the completion value (node -p)
- jsc build FILE -o OUT writes the module (-o - for stdout)
- compile errors exit 2, uncaught/runtime failures exit 1
- remove --emit; convert integration suites to the new interface
…tests (bkd/lu7vym8f) [l1-mn1fy0fr-20261018041900]
Member reads, writes, compound assignments and user method calls on a
null/undefined receiver now throw a branded TypeError with V8's message
text via an out-of-line __nullish_throw helper; the hot path is a single
NaN-box tag compare, skipped for statically non-nullish receivers.
An indirect call in a function-less program throws the not-a-function
TypeError instead of emitting an invalid call_indirect.
0/0 produces the sign-bit-set NaN 0xFFF8.. on x86, which emit_is_number
treated as a boxed non-number, so a computed NaN compared by identity:
`a === a` was true and `a !== a` false. The negation itself was already
exact. Classify a jsval as a number iff its bits, as a signed i64, are at
or below the largest tag-0 NaN; every boxed immediate and heap pointer
lies strictly above that bound.

Add tests/equality.rs with the computed-NaN regressions and a negation
matrix for !== / != over mixed operand types.
__alloc now grows linear memory when the bump would pass memory.size,
by max(needed, current) pages with an exact-size fallback; the fast path
is one i64 compare. Memory is capped at 512 MiB (module maximum). A
failed guest or host grow stores a sticky -1 sentinel in __heap_ptr and
traps; host imports trap on it too, and the runner reports it as
'Uncaught RangeError: out of memory' without a wasm backtrace. Host
allocators share the guest growth policy via heap_reserve.
Run each assembled test under jsc in a std-only worker pool with a
per-test timeout, stderr captured to a file. The CLI prints the
per-directory report, writes per-test results, and supports
--check / --update-baseline / --history against the committed
baseline and history files.
…hecked

- Drop the compile-time rejection of every generic method call in programs
  without functions and of null-literal receivers; such calls now reach the
  runtime nullish / not-a-function guards (no funcref table => the branded
  not-a-function TypeError instead of an invalid call_indirect).
- Support member update expressions (o.p++, --o[k], ...) as an IndexOpSet
  with an OpSetKind: the current value is converted with ToNumber, postfix
  yields the old numeric value, and a nullish base throws the read TypeError.
- Guard .length and builtin method receivers (array/string methods, array
  HOFs, push/pop, Map/Set and Date methods, getTime) with a NullishCheck node
  so a null/undefined receiver throws "Cannot read properties of ...".
…rror, NaN equality) [l1-mn1fy0fr-20261018041900]
Drop the compile-time arity check on direct calls to declared functions.
The uniform ABI already pads missing arguments with undefined (so defaults
apply) and the arity scan widens K to cover every call site, so surplus
arguments are evaluated for side effects and ignored or gathered by a rest
parameter. Remove the now-unused FnSig arity fields and the test asserting
the rejection.
parse() now takes a Dialect. Dialect::Ts parses with swc's TypeScript
syntax and runs resolver + strip so codegen only sees plain ES. The CLI
picks Ts for .ts/.mts/.cts files, rejects .tsx, and keeps -e/-p and all
other files as JavaScript.
….0.104, wasm-bindgen 0.2.129

Also refresh Cargo.lock with cargo update so every transitive dependency
is at its latest semver-compatible version.
…dgen + cargo update (bkd/dryfefmz) [l1-mn1fy0fr-20261018041900]
…cies and CI actions to latest [l1-mn1fy0fr-20261018041900]
…tores

Fixed-shape objects silently dropped writes to keys they do not own, and
the array arm of computed stores had no bounds or key check, so an
out-of-range or non-index key wrote past (or onto) the element area.

__obj_set now returns a hit flag; a miss calls the new out-of-line
__prop_throw helper which raises a branded TypeError
("Cannot add property 'k': dynamic properties are not supported yet")
and unwinds through the existing propagate path. Array stores accept
only integer indices in [0, length); an index at/past the length raises
"Cannot grow array by index assignment yet", any other key (or any
other heap receiver) raises the add-property error. Nothing is written
on a miss.
Absent-property writes and array index growth now throw, so tests that
passed only because such writes were silently dropped regress (41), and
three function tests expecting a TypeError on .caller/.arguments writes
now pass. README/DESIGN restate the fixed-shape limitation as current
behaviour and carry the new 5195/22992 result.
…s and array OOB writes (bkd/zuppd6lq) [l1-mn1fy0fr-20261018041900]
…nd array growth by index [l1-mn1fy0fr-20261018041900]
@wuchendi wuchendi self-assigned this Oct 19, 2026
@wuchendi
wuchendi merged commit cdf1962 into main Oct 19, 2026
3 checks passed
@wuchendi
wuchendi deleted the feat/real-usable-wave1 branch October 19, 2026 07:05
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