An ahead-of-time JavaScript/TypeScript → WebAssembly compiler in Rust, conformance tracked by test262.
$ jsc -p "[1,2,3,4].filter(x => x % 2 === 0).map(x => x * 10).reduce((a,b) => a+b, 0)"
60
$ jsc -e 'console.log("sum", [1,2,3].reduce((a,b) => a+b, 0))'
sum 6- A compiler, not an interpreter. Every program becomes a validated WebAssembly module (emitted with
wasm-encoder). The CLI runs it onwasmtimeor writes the bytes out withjsc build. - An embeddable library. The whole pipeline lives in the
jsc-corecrate;jscis a thin CLI over it. - An in-browser compiler.
crates/jsc-wasmcompilesjsc-coreitself to wasm (wasm-bindgen); the@cdlab/jscpackage (packages/jsc) wraps it withcompile(src)/run(src)for the browser and Node, andapps/jsc-webis a playground built on that package. - TypeScript by type stripping only — no type checking.
.ts/.mts/.ctsfiles have their types removed by swc's TypeScript strip pass before codegen; see TypeScript.
From source (Rust 1.99+):
git clone https://github.com/cdLab996/jsc.git
cd jsc
cargo install --path crates/jsc # installs the `jsc` binary
# or: cargo build --release # -> target/release/jscPushing a v* tag builds a static Linux x86_64 binary (jsc-linux-x64.tar.gz) and attaches it to a GitHub Release; other platforms build from source.
The browser package: bun install, then bun run build:wasm (builds crates/jsc-wasm into packages/jsc/wasm) and bun run dev:web for the playground.
jsc follows node's command-line shape:
jsc program.js # compile and run a file; prints only console output
jsc -e 'console.log(40 + 2)' # compile and run an inline source string
jsc -p '40 + 2' # run inline source and print its completion value
jsc build program.js -o out.wasm # write the WebAssembly module (-o - writes to stdout)
jsc program.ts # TypeScript: types are stripped, not checkedconsole.log streams to stdout while the module runs; -p prints the completion value afterwards, Node-style ([ 1, 4, 9 ]). Exactly one of FILE, -e, -p is accepted.
| Exit code | Meaning |
|---|---|
0 |
success |
1 |
uncaught exception (Uncaught <value> on stderr), runtime trap, or I/O failure |
2 |
compile error (a caret diagnostic pointing at the source span) or invalid command line |
$ jsc -e 'throw new TypeError("boom")'
Uncaught TypeError: boom
$ echo $?
1The dialect is chosen by FILE extension:
.ts,.mts,.cts— TypeScript. swc parses the file as TypeScript and its strip pass erases type syntax (annotations,interface,type, generics,as,!); nothing is type-checked..tsx— rejected (exit2); JSX is not supported.- any other extension, and all
-e/-pinput — JavaScript, so type syntax there is a compile error.
enum and namespace are lowered by the strip pass to var-based JavaScript and run like any other declaration. Invalid input in either dialect — including errors swc recovers from, such as strict-mode early errors — is a compile error (exit 2). The @cdlab/jsc package compiles JavaScript only.
test262 is the primary metric. crates/jsc-test262 runs the official suite at a pinned commit against the release jsc binary:
scripts/test262-fetch.shchecks out tc39/test262 into.tmp/test262.- Tests run against the official harness files in that checkout, unmodified:
"use strict";+sta.js+assert.js+ the test's includes + the test source, compiled and run asjsc FILE. Includes that do not compile make their tests ineligible. tests/test262/unsupported-features.txtlists the feature tags that make a test ineligible (Proxy, Reflect, BigInt, RegExp, typed arrays, WeakMap/WeakRef, Intl, Temporal, …). Tests flaggedmoduleornoStrict(andrawtests flaggedasync) are ineligible as well; otherasynctests run with the officialdoneprintHandle.jsand pass onTest262:AsyncTestComplete.tests/test262/baseline.txtis the set of passing tests;tests/test262/history.tsvrecordspass/eligible/totalper commit from a full run (--history).
Current result (last row of tests/test262/history.tsv): 28037 pass / 32889 eligible (85.2%).
bash scripts/test262-fetch.sh
cargo build --release -p jsc
cargo run --release -p jsc-test262 # full run with a per-directory report
cargo run --release -p jsc-test262 -- built-ins/Array # only tests under a path prefix
cargo run --release -p jsc-test262 -- --check # fail if any baseline test regressed
cargo run --release -p jsc-test262 -- --update-baseline # rewrite baseline.txt (only the filtered prefixes, if any)
cargo run --release -p jsc-test262 -- --history # append a row to history.tsvCI (.github/workflows/):
ci.yml— manual only (workflow_dispatch: the Actions tab orgh workflow run ci.yml --ref <branch>):cargo fmt --check,typos,cargo clippy(warnings denied),cargo nextest, doctests,cargo shear,cargo deny; plus a test262 job that fetches test262, runs the M1 harness tests (crates/jsc/tests/test262_harness.rs: the officialsta.js+assert.jscompile and run), and runsjsc-test262 --checkagainst the baseline.web.yml— on push tomainand pull requests that touchcrates/**,packages/**,apps/**,scripts/**, Cargo or bun manifests (and manual dispatch): builds@cdlab/jsc, runsbiomelint, builds the playground, and runs the package tests.release.yml— onv*tags (and manual dispatch): builds the static musl binary and publishes the release.
| Area | What works |
|---|---|
| Values | number, boolean, undefined, null, string (Latin-1 / UTF-16, spec .length and indexing), symbol, arrays, objects, functions, Map, Set, Promise, Date, generators |
| Operators | comma, arithmetic, bitwise and shift, comparison, ==/=== and negations, logical and ??, typeof, instanceof, in, delete, compound assignment, ++/--, optional chaining ?.; ToPrimitive / ToNumber / ToString call user valueOf / toString |
| Bindings | var hoisted to function scope (top-level var and function declarations are global-object properties), block-scoped let/const (assigning a const is a compile error); array/object destructuring with defaults and rest; spread in array literals and calls |
| Objects | dynamic properties with a prototype chain and property descriptors (writable / enumerable / configurable, accessors), extensibility, seal / freeze; object literals with computed keys, methods, getters/setters and __proto__; arrays with holes (read through the prototype chain), growth by index, sparse storage and length writes |
| Control flow | if, for, while, do/while, for-of, for await, for-in, switch, labels, ternary |
| Functions | declarations, expressions, arrows, closures (capture by reference), default and rest parameters, arguments; functions are objects; call/apply; missing arguments are undefined, extra arguments are ignored; generator and async functions, expressions, methods and async generators, top-level await; bind; parameter TDZ |
| Classes | class declarations and expressions, fields, methods, constructors, extends any constructor (including builtins)/super, new.target, static methods and fields, getters/setters |
| Built-ins | function objects on real prototypes, usable as values: console.log, Symbol (registry, well-known symbols and their hooks), the iteration protocol with Iterator and its helpers, Math, Object, String/Number/Boolean (and wrapper objects), parseInt/parseFloat, isNaN/isFinite, globalThis, Object reflection (defineProperty(ies), getOwnPropertyDescriptor(s), create, getPrototypeOf / setPrototypeOf, assign, keys / values / entries, …), the ES2025 Array, String, Number and Math methods, Map/Set, JSON.parse/stringify (reviver, replacer, space), Date |
| Async | Promise (new Promise, resolve/reject, .then/.catch/.finally, all/allSettled/any/race) with a microtask queue; async iteration (for await, async generators, Array.fromAsync) |
| Errors | throw, try/catch/finally; Error and the six native error types with AggregateError, with .name, .message, cause, .stack and instanceof; engine errors (null member access, calling a non-function, unresolved names, …) are catchable error objects; running out of memory ends the run with RangeError |
Unsupported syntax is rejected at compile time with a source-span diagnostic; calling a missing method throws TypeError at run time.
source ──swc──▶ AST ──codegen──▶ IR ──emit──▶ .wasm ──run──▶ result
- frontend — parses source with
swc. - codegen — lowers the AST to a small IR: scopes, closure capture analysis, generator/async state machines. Top-level bindings captured by top-level functions live in module cells.
- emit — IR → wasm. Every value is a NaN-boxed
i64(numbers are rawf64bits, with NaN canonicalized on every rebox through__box_f64; other kinds are tagged in the quiet-NaN space). Strings, arrays, objects, closures and environments live in linear memory behind a type-byte header, allocated by__alloc, a bump allocator that grows memory on demand up to 512 MiB. Objects keep their own properties in interned-key tables and link a prototype; builtins live on a static realm image, with guarded inline fast paths for common calls. Exceptions use manual unwinding (no wasm exception-handling proposal).Map/Setkeys compare with SameValueZero (__same_value_zero). - run — instantiates the module on
wasmtime, provides the host imports, drains the microtask queue, and formats the result.
Details are in DESIGN.md.
- Strict-mode semantics only. Sloppy-mode constructs such as
withor assigning an undeclared variable are compile errors. - No GC. The heap is bump-allocated and never freed; the 512 MiB cap ends a run with
RangeError: out of memory. - Not self-contained. The module imports seven host functions from the
envnamespace (print,__convert,__method,__promise,__then,__now,__parse_int), provided by thewasmtimehost injsc-coreand by the JS shim in@cdlab/jsc; it is not a WASI module. - Missing built-ins. No
RegExp(regex literals are rejected),Proxy,Reflect,BigInt,WeakMap/WeakSet/WeakRef, typed arrays,evalor theFunctionconstructor; no ES modules.
The built-in library and UTF-16 strings, self-contained WASI modules with GC, the remaining language features (ES modules, RegExp, …), performance work, and productization; see DESIGN.md.