JS Ecosystem Compatible
Compile existing code without rewriting it into a restricted subset, and stay compatible with the full JS ecosystem, the web, and NPM. Access Web APIs without writing glue code.
JS 2 is a research preview of an open source compiler focused on compiling JavaScript to WebAssembly ahead of time, developed at Loopdive.
The mission of the JS² compiler project is to make WebAssembly a drop-in deployment target for existing JavaScript and npm packages, running modules without embedding a JS runtime and enabling more secure, flexible dependency management.
The project is motivated by the idea that JavaScript, despite being a dynamic language, can be compiled efficiently ahead of time without limiting it to an incompatible subset or shipping a full JS runtime like V8 or SpiderMonkey.
The project is guided by the following goals:
Compile existing code without rewriting it into a restricted subset, and stay compatible with the full JS ecosystem, the web, and NPM. Access Web APIs without writing glue code.
Deploy compiled JavaScript modules in standalone WebAssembly runtimes such as Wasmtime without embedding a JS engine or interpreter, reducing module size and runtime overhead.
Reduce supply chain attack surface by isolating JavaScript modules from each other inside the same application and keeping filesystem, network, DOM, and globals behind explicit imports.
Dynamic module linking allows to make code more modular so consumers can choose implementations, control upgrades, and swap dependencies for each environment.
Which JS language features compile to WebAssembly today, which still rely on host support, and which are still on the roadmap. Status is derived from ECMAScript Test262 pass rates and known compiler limitations.
const s = "hello"; const n = 42; const b = true; const x = null;
(func $__module_init string.const "hello" global.set $s f64.const 42 global.set $n i32.const 1 ;; true global.set $b)
const sum = a + b; const eq = x === y; const ok = a > 0 && b > 0; const bits = n | 0xFF;
(func $example (param f64 f64)
(result f64)
local.get 0
local.get 1
f64.add ;; a + b
)
;; x === y → f64.eq (or ref.eq)
;; n | 0xFF → i32.or
typeof x === "string"; obj instanceof Array;
;; typeof x === "string" ;; compiles to tag check on struct local.get $x ref.test (ref $String) ;; instanceof → ref.test
const obj = { a: 1, b: 2 };
delete obj.b;
;; delete obj.b local.get $obj ref.null extern struct.set $Obj $b
const x = (1, 2, 3); // x === 3
f64.const 1 drop f64.const 2 drop f64.const 3
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) continue outer;
}
}
(block $outer
(loop $outer_loop
(block $inner
(loop $inner_loop
;; if j === 1
br $outer_loop
))
br $outer_loop
))
for (const key in obj) {
console.log(key);
}
local.get $obj call $Object.keys local.set $keys ;; iterate keys array
function legacy() {
return arguments.length;
}
// prefer rest params: (...args)
eval("1 + 2"); // delegated to host
with (obj) { x; } // not supported
let count = 1; const name = "hello"; var legacy = true;
(func $__module_init (local $count f64) (local $name externref) (local $legacy i32) f64.const 1 local.set $count string.const "hello" local.set $name i32.const 1 local.set $legacy)
function greet(name) {
return "Hi " + name;
}
const add = (a, b) => a + b;
(func $greet (param (ref null $str))
(result (ref null $str))
string.const "Hi "
local.get 0 ;; name
string.concat
return)
if (x > 0) {
handle();
} else {
fallback();
}
for (let i = 0; i < 10; i++) {
process(i);
}
(func $test (param f64)
(result f64)
local.get 0
f64.const 0
f64.gt
(if (result f64)
(then local.get 0 return)
(else local.get 0 f64.neg return)))
try {
riskyOp();
} catch (e) {
handle(e);
} finally {
cleanup();
}
(func $safe (result f64)
(try
(do call $riskyOp)
(catch 0
local.set $e
f64.const -1
return)))
throw new Error("something went wrong");
(func $__module_init i32.const 1 string.const "something went wrong" struct.new $Error throw 0)
const obj = {
name: "js2wasm",
version: 1,
greet() { return this.name; }
};
(type $obj (struct
(field $__tag i32)
(field $name externref)
(field $version f64)))
(func $obj_greet (param (ref null $obj))
(result externref)
local.get 0
struct.get $obj $name
return)
"hello".toUpperCase(); // "HELLO"
"a,b,c".split(",");
"hello".slice(1, 3); // "el"
(func $__module_init ;; "hello".toUpperCase() string.const "hello" call $string.toUpperCase ;; host ;; "hello".slice(1, 3) string.const "hello" i32.const 1 i32.const 3 string.substring)
Math.max(1, 2);
parseInt("42");
(3.14).toFixed(1);
(func $__module_init (local $a f64) (local $b f64) f64.const 1 local.set $a f64.const 2 local.set $b local.get $a local.get $b f64.max drop)
const obj = JSON.parse('{"a": 1}');
const str = JSON.stringify({ a: 1 });
(func $__module_init global.get 0 call $JSON.parse global.set $obj)
throw new TypeError("expected string");
throw new RangeError("out of bounds");
(func $__module_init i32.const 2 string.const "expected string" struct.new $TypeError throw 0)
const doubled = [1, 2, 3].map(x => x * 2); const sum = arr.reduce((a, b) => a + b, 0); const found = arr.find(x => x > 5);
(func $__module_init ;; [1, 2, 3].map(x => x... f64.const 1 f64.const 2 f64.const 3 array.new_fixed 3 ref.func $callback call $__arr_map
/hello/i.test("Hello World");
"abc123".match(/\d+/);
(func $__module_init global.get $re string.const "Hello World" call $regexp_test)
const obj = {
_v: 0,
get value() { return this._v; },
set value(v) { this._v = v; }
};
(func $get_value (param (ref null $obj))
(result f64)
local.get 0
struct.get $obj $_v
)
(func $set_value (param (ref null $obj)) (param f64)
local.get 0
local.get 1
struct.set $obj $_v)
Object.defineProperty(obj, "x", {
enumerable: false,
writable: false
});
const add = (a, b) => a + b; const square = x => x * x; const greet = () => "hello";
(func $add (param f64 f64)
(result f64)
local.get 0
local.get 1
f64.add)
(func $square (param f64)
(result f64)
local.get 0
local.get 0
f64.mul)
const msg = `Hello ${name}, you are ${age}!`;
(func $__module_init string.const "Hello " local.get $name string.concat string.const "!" string.concat)
const { name, age } = person;
const [first, ...rest] = items;
const { a: x = 0 } = opts;
;; typed object → direct struct ... (func $__module_init local.get $obj struct.get $Obj $a ;; a local.get $obj struct.get $Obj $b) ;; b
const merged = [...a, ...b];
const clone = { ...original };
function sum(...args) { }
(func $__module_init ;; [...arr, 4] f64.const 1 f64.const 2 f64.const 3 f64.const 4 array.new_fixed $f64arr 4)
function greet(name = "world") { }
(func $greet (param externref)
(result externref)
local.get 0
ref.is_null
(if (then
global.get 1
local.set 0))
local.get 0
return)
const key = "id";
const obj = { [key]: 42 };
(func $__module_init
;; { [key]: 42 }
global.get $key ;; "id"
f64.const 42
struct.new $obj)
for (const item of iterable) { }
(func $__module_init
;; for (const x of arr)
local.get $arr
struct.get $Vec $data
struct.get $Vec $len
(loop $for_of
local.get $i
array.get $data
;; ... process x
br_if $for_of))
function* range(n) {
for (let i = 0; i < n; i++) {
yield i;
}
}
(func $range (param f64)
(result (ref $Gen))
;; yield values into WasmGC array
array.new_default $f64arr 0
local.set $buf
(loop $loop
local.get $i
local.get 0 ;; n
i32.lt_s
(if (then
;; yield i → push to buffer
local.get $i
;; i++, br $loop
)))
local.get $buf
struct.new $Generator)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + " speaks";
}
}
class Dog extends Animal { }
(type $Animal (struct
(field $__tag i32)
(field $name externref)))
(func $Animal_new (param externref)
(result (ref null $Animal))
struct.new $Animal
local.get 0
struct.set $Animal $name)
const m = new Map();
m.set("key", 42);
const s = new Set([1, 2, 3]);
s.has(2); // true
(func $__module_init call $Map_new global.set $m global.get $m string.const "key" f64.const 42 call $Map_set)
const id = Symbol("id");
obj[id] = 42;
(func $__module_init global.get $nextId i32.const 1 i32.add global.set $nextId global.get $nextId global.set $id)
const buf = new ArrayBuffer(16); const view = new Int32Array(buf); view[0] = 42;
(func $__module_init i32.const 16 call $ArrayBuffer_new call $Int32Array_from global.set $view global.get $view i32.const 0 f64.const 42 call $TypedArray_set)
import { foo } from "./mod";
export function bar() { return 1; }
(func $bar (export "bar")
(result f64)
f64.const 1
return)
new Proxy(target, handler); // not supported
promise.then(v => v + 1); // not yet
const mod = await import("./module"); // not yet
(func $fetchData (result f64) f64.const 42 call $Promise.resolve call $__await return)
Object.entries({ a: 1, b: 2 });
// [["a", 1], ["b", 2]]
Object.values({ x: 10 });
// [10]
(func $__module_init f64.const 1 f64.const 2 struct.new $obj extern.convert_any call $Object.entries)
new SharedArrayBuffer(1024); // not supported
const clone = { ...original,
extra: 1 };
const { a, ...rest } = obj;
(func $__module_init
;; { ...original, extra: 2 }
local.get $original
struct.get $Obj $x
f64.const 2
struct.new $Clone)
for await (const chunk of stream) { }
(func $process (param externref)
local.get 0
call $__iterator
local.set $iter
(loop $for_await
local.get $iter
call $__iterator_next
;; ... process chunk
br_if $for_await))
const name = user?.profile?.name;
(func $test (param externref)
(result externref)
local.get 0
ref.is_null
(if (result externref)
(then ref.null extern)
(else local.get 0
call $__extern_get_name)))
const val = input ?? "default";
(func $test (param f64)
(result f64)
local.get 0 ;; x
call $__is_nullish
(if (result f64)
(then f64.const 0)
(else local.get 0)))
globalThis.console;
(func $__module_init call $__get_globalThis global.set $g)
const big = 9007199254740993n; const sum = big + 1n;
(func $__module_init i64.const 9007199254740993 global.set $big global.get $big i64.const 1 i64.add global.set $sum)
const mod = await import("./module"); // not yet
new WeakRef(obj); // not supported
class Counter {
count = 0;
#secret = 42;
static instances = 0;
}
(type $Counter (struct
(field $__tag i32)
(field $count f64)))
(func $Counter_new
(result (ref null $Counter))
i32.const 0 ;; __tag
f64.const 0 ;; count = 0
struct.new $Counter)
throw new Error("fail", { cause: origErr });
(func $fail i32.const 1 string.const "root" struct.new $Error ;; cause i32.const 1 string.const "fail" struct.new $ErrorCause throw 0)
[1, 2, 3].at(-1); // 3 "hello".at(0); // "h"
(func $__module_init ;; [1,2,3].at(-1) local.get $arr i32.const -1 local.get $len i32.add array.get $data) ;; → 3
const data = await fetch(url);
export { data };
[1, 2, 3].includes(2); // true
;; linear scan over WasmGC array local.get $arr struct.get $Vec $data (loop $scan array.get $data local.get $target f64.eq br_if $found br $scan)
const sq = 2 ** 10; // 1024
f64.const 2 f64.const 10 call $Math.pow
try { riskyOp(); }
catch { handleError(); }
(try
(do call $riskyOp)
(catch_all
call $handleError))
[[1, 2], [3]].flat(); // → [1, 2, 3]
const obj = Object.fromEntries( [["a", 1], ["b", 2]] );
[1, 2, 3, 2].findLast(x => x === 2); // → 2 (last match)
const sorted = arr.toSorted(); const rev = arr.toReversed();
#!/usr/bin/env node
console.log("hello");
;; hashbang stripped at parse time ;; no Wasm output for comments
const { promise, resolve, reject }
= Promise.withResolvers();
const buf = new ArrayBuffer(8,
{ maxByteLength: 16 });
buf.resize(16);
/[\p{Letter}--[a-z]]/v.test("A");
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // {1,2,3,4}
function* nums() { yield 1; yield 2; yield 3; }
nums().filter(x => x > 1)
.take(1)
.toArray();
/(?<v>a)|(?<v>b)/.exec("b");
// v === "b"
console.log(x); // undefined var x = 5;
;; var hoisted to function scope (local $x f64) f64.const 0 local.set $x ;; ... later f64.const 5 local.set $x
function f() {
return arguments.callee;
}
obj.__proto__ = parent;
"hello".substr(1, 3); // "ell" // use .slice() or .substring()
const n = 0777; const ok = 0o777;
escape("hello world");
// use encodeURIComponent()
function f() {
return f.caller;
}
"text".bold(); // deprecated
"text".anchor("name");
/(\d+)/.exec("abc123");
RegExp.$1; // "123" — deprecated
const now = Temporal.Now.instant();
const date = Temporal.PlainDate
.from("2026-04-06");
@logged
class MyClass {
@bound method() { }
}
match (value) {
when ({ x, y }): return x + y;
when (String): return value.length;
default: return 0;
}
When running outside of a JS engine like found in browsers or Node.js, compatibility today typically means embedding a JS interpreter inside the WebAssembly module. This increases module size and slows execution.
Runtime execution speed and module load time relative to plain JavaScript, measured in browser and Node.js environments.
Write regular JavaScript. The compiler produces a .wasm binary that runs in any WebAssembly host with GC support — browser, server, edge. No interpreter embedded; garbage collection is handled by the host via WasmGC. When deployed with explicit imports, the module boundary can reduce ambient access and limit supply-chain attack surface.
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1)
+ fibonacci(n - 2);
}
export function run() {
return fibonacci(10);
}
app.js
;; simplified — actual output includes
;; box/unbox for externref returns
(func $fibonacci (param f64) (result externref)
local.get 0
f64.const 1
f64.le
if
local.get 0
call $__box_number
return
end
local.get 0 f64.const 1 f64.sub
call $fibonacci call $__unbox_number
local.get 0 f64.const 2 f64.sub
call $fibonacci call $__unbox_number
f64.add
call $__box_number)
(func $run (export "run") (result externref)
f64.const 10
call $fibonacci)
app.wat
DOM access and browser APIs compile to typed host imports. The compiler generates the bindings — you just write normal JavaScript.
const el = document.createElement("div");
el.textContent = "Hello from Wasm";
document.body.appendChild(el);
dom.js
(import "env" "global_document"
(func $doc (result externref)))
(import "env" "Document_createElement"
(func $createElement
(param externref externref externref)
(result externref)))
(import "env" "Element_set_textContent"
(func $setTC (param externref externref)))
(import "env" "Node_appendChild"
(func $appendChild
(param externref externref)
(result externref)))
(import "env" "Document_get_body"
(func $getBody (param externref)
(result externref)))
(func $__module_init
call $doc
string.const "div"
call $createElement
local.tee $el
string.const "Hello from Wasm"
call $setTC
call $doc
call $getBody
local.get $el
call $appendChild
drop)
dom.wat
// Compile from JavaScript source
import { compile } from "js2wasm";
import { buildImports } from "js2wasm/runtime";
const result = compile(sourceCode, {
fileName: "app.js"
});
// Instantiate the compiled module
const imports = buildImports(
result.imports, undefined, result.stringPool
);
const { instance } = await WebAssembly
.instantiate(result.binary, imports);
console.log(instance.exports.run()); // → 55
run.ts
compile()
produces a
.wasm
binary and import manifest. Use
buildImports()
to create the host import object, then instantiate with the standard WebAssembly API.
Architecture Decision Records (ADRs) document the non-obvious choices behind js2wasm — why WasmGC over linear memory, why ahead-of-time compilation over a JIT, how closures and objects are laid out. Each record captures the context, the decision, and its consequences in a few hundred words. Together they make the design auditable for external contributors and reviewers. Read the index →
The roadmap is to first establish compatibility with existing code, then expand host support, optimize performance, and strengthen security.