JS 2 is a research preview of an open source compiler focused on compiling JavaScript to WebAssembly ahead of time, developed at Loopdive.


Mission: Next generation JavaScript.

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:

JavaScript logo

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.

WebAssembly logo

No Embedded Runtime

Deploy compiled JavaScript modules in standalone WebAssembly runtimes such as Wasmtime without embedding a JS engine or interpreter, reducing module size and runtime overhead.

🔒

Sandbox Untrusted Code

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.

🧩

Plug & Play

Dynamic module linking allows to make code more modular so consumers can choose implementations, control upgrades, and swap dependencies for each environment.


Goal: 100% JavaScript compatibility.

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.

Compatibility Test Report

ECMAScript Conformance Pass Rate Over Time

Pass rate Tests passed Total tests run
ES3 / Core
Primitive types (string, number, boolean, null, undefined) All primitive value types and coercion
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)
Operators (arithmetic, comparison, logical, bitwise) All standard operators including ternary
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 / instanceof Runtime type checking operators
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
delete operator Remove object properties
const obj = { a: 1, b: 2 };
delete obj.b;
;; delete obj.b
local.get $obj
ref.null extern
struct.set $Obj $b
Comma operator Evaluate expressions left to right
const x = (1, 2, 3); // x === 3
f64.const 1
drop
f64.const 2
drop
f64.const 3
Labeled statements (break / continue) Named loop targets for break and continue
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-in Iterate over object property names
for (const key in obj) {
  console.log(key);
}
local.get $obj
call $Object.keys
local.set $keys
;; iterate keys array
arguments object (full) Legacy arguments — partial, rest params preferred
function legacy() {
  return arguments.length;
}
// prefer rest params: (...args)
The full arguments object is partially supported. Rest parameters (...args) are preferred and fully compiled.
host eval() Dynamic code evaluation via JS host import
eval("1 + 2"); // delegated to host
Compiles to a host import that delegates to the JS engine. Requires a JS host runtime; not available in standalone Wasm.
with statement Dynamic scope extension
with (obj) { x; } // not supported
Disallowed in strict mode. All modules run strict.
ES5
Variables (var, let, const) Block-scoped and function-scoped variable declarations
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)
Functions & closures Named functions, expressions, and lexical closures
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)
Control flow Branching, loops, and switch statements
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 / catch / finally Exception handling with optional finally block
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 Throw custom and built-in error objects
throw new Error("something went wrong");
(func $__module_init
  i32.const 1
  string.const "something went wrong"
  struct.new $Error
  throw 0)
Objects Literals, property access, methods, and shorthand syntax
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)
Strings String methods, concatenation, and manipulation
"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)
Numbers Math operations, parseInt, Number methods
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)
host JSON Parse and stringify JSON data
const obj = JSON.parse('{"a": 1}');
const str = JSON.stringify({ a: 1 });
(func $__module_init
  global.get 0
  call $JSON.parse
  global.set $obj)
Error types Error, TypeError, RangeError, SyntaxError and more
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)
host Arrays Array methods and iteration helpers
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 =&gt; x...
  f64.const 1  f64.const 2  f64.const 3
  array.new_fixed 3
  ref.func $callback
  call $__arr_map
Most built-in methods work. Some iterator edge cases and sparse array handling incomplete.
host Regular expressions Pattern matching and string search
/hello/i.test("Hello World");
"abc123".match(/\d+/);
(func $__module_init
  global.get $re
  string.const "Hello World"
  call $regexp_test)
Basic patterns work. Named groups and lookbehind partially supported.
Property accessors (get / set) Getter and setter property definitions
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)
Basic get/set works. Object.defineProperty partially supported.
Object.defineProperty (full) Full property descriptor configuration
Object.defineProperty(obj, "x", {
  enumerable: false,
  writable: false
});
Property descriptor system not yet fully emitted in Wasm structs.
ES2015
Arrow functions Concise function syntax with lexical this
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)
Template literals String interpolation with backtick syntax
const msg = `Hello ${name}, you are ${age}!`;
(func $__module_init
  string.const "Hello "
  local.get $name
  string.concat
  string.const "!"
  string.concat)
Destructuring Extract values from arrays and objects
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
Spread / rest operators Expand iterables and collect arguments
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)
Default parameters Fallback values for function parameters
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)
Computed property names Dynamic property keys in object literals
const key = "id";
const obj = { [key]: 42 };
(func $__module_init
  ;; { [key]: 42 }
  global.get $key          ;; "id"
  f64.const 42
  struct.new $obj)
for-of Iterate over iterable objects
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))
Generators (function*, yield) Pausable functions that produce sequences
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)
Classes Class declarations with inheritance
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)
Constructor, methods, extends, super work. Dynamic prototype lookup is partial.
host Map / Set Key-value and unique-value collections
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)
Core operations work. Some iteration edge cases incomplete.
host Symbol Unique, immutable primitive identifiers
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)
Creation and basic use work. Well-known symbols (Symbol.iterator) partial.
host TypedArray / ArrayBuffer Binary data buffers and typed views
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)
Int8 through Float64 arrays work. BigInt64Array not yet.
Modules (import / export) ES module system for code organization
import { foo } from "./mod";
export function bar() { return 1; }
(func $bar (export "bar")
    (result f64)
  f64.const 1
  return)
Static imports work. Dynamic import() not yet.
Proxy / Reflect Object behavior interception and reflection
new Proxy(target, handler); // not supported
Requires runtime trap dispatch. Not AOT-compilable.
host Promise .then / .catch / .finally Promise chaining and error handling
promise.then(v => v + 1); // not yet
Compiles but async callbacks do not execute. Promise.resolve/all/race work.
ES2017
host async / await Asynchronous functions with synchronous-style syntax
const mod = await import("./module"); // not yet
(func $fetchData (result f64)
  f64.const 42
  call $Promise.resolve
  call $__await
  return)
Object.entries / values Extract entries or values from objects
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)
host SharedArrayBuffer / Atomics Shared memory and atomic operations
new SharedArrayBuffer(1024); // not supported
Requires shared Wasm linear memory.
ES2018
Object spread / rest Object spread in literals and rest in destructuring
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)
host Async iteration (for-await-of) Iterate over async data sources
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))
Basic patterns work. Some async generator edge cases incomplete.
ES2020
Optional chaining (?.) Safe property access on nullable values
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)))
Nullish coalescing (??) Default values for null or undefined
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)))
host globalThis Universal global object reference
globalThis.console;
(func $__module_init
  call $__get_globalThis
  global.set $g)
host BigInt Arbitrary precision integer arithmetic
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)
Basic arithmetic works. BigInt typed arrays not yet.
host Dynamic import() Load modules at runtime on demand
const mod = await import("./module"); // not yet
Requires a runtime module loader.
ES2021
host WeakRef / FinalizationRegistry Weak references and GC callbacks
new WeakRef(obj); // not supported
GC-observable. Not available in standard WasmGC today.
ES2022
Class fields (public, private, static) Declarative field syntax in classes
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)
Error.cause Chain errors with a cause property
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)
Array.at / String.at Relative indexing with negative support
[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
Top-level await Await at module scope without async wrapper
const data = await fetch(url);
export { data };
Requires module-level async execution model. Not yet supported.
ES2016
Array.prototype.includes Check if array contains a value
[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)
Exponentiation operator (**) Power operator as infix syntax
const sq = 2 ** 10; // 1024
f64.const 2
f64.const 10
call $Math.pow
ES2019
Optional catch binding Omit the error parameter in catch
try { riskyOp(); }
catch { handleError(); }
(try
  (do call $riskyOp)
  (catch_all
    call $handleError))
host Array.prototype.flat / flatMap Flatten nested arrays
[[1, 2], [3]].flat();
// → [1, 2, 3]
Basic flat() works via host import. Deep flattening and flatMap partial.
host Object.fromEntries Create object from key-value pairs
const obj = Object.fromEntries(
  [["a", 1], ["b", 2]]
);
Uses host import to construct object from entries array.
ES2023
host Array.findLast / findLastIndex Search arrays from the end
[1, 2, 3,
  2].findLast(x => x === 2);
// → 2 (last match)
Iterates WasmGC array from end. Callback via host import.
host Change array by copy (toSorted, toReversed, toSpliced) Immutable array transformations
const sorted = arr.toSorted();
const rev = arr.toReversed();
Immutable array operations not yet implemented. Requires new array allocation + copy.
Hashbang (#!) comments Unix shebang line support
#!/usr/bin/env node
console.log("hello");
;; hashbang stripped at parse time
;; no Wasm output for comments
ES2024
host Promise.withResolvers Create Promise with external resolve/reject
const { promise, resolve, reject }
  = Promise.withResolvers();
Static Promise method not yet implemented. Requires host import.
host Resizable ArrayBuffer Growable and shrinkable buffers
const buf = new ArrayBuffer(8,
  { maxByteLength: 16 });
buf.resize(16);
Growable buffers not yet supported. Requires linear memory integration.
RegExp v flag Set notation in character classes
/[\p{Letter}--[a-z]]/v.test("A");
Unicode set notation in character classes. Requires updated regex host.
ES2025
host Set methods (union, intersection, difference) Built-in set operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // {1,2,3,4}
ES2025 Set methods. Requires host import for Set operations.
host Iterator helpers (map, filter, take) Lazy iterator transformations
function* nums() { yield 1; yield 2; yield 3; }
nums().filter(x => x > 1)
      .take(1)
      .toArray();
Lazy iterator protocol. Requires iterator helper host imports.
RegExp duplicate named groups Same group name in alternatives
/(?<v>a)|(?<v>b)/.exec("b");
// v === "b"
Same named group in alternatives. Requires regex engine update.
Legacy / Deprecated
var hoisting Function-scoped variable declarations hoisted to top
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
arguments.callee Reference to currently executing function
function f() {
  return arguments.callee;
}
Forbidden in strict mode. All modules run strict.
__proto__ accessor Legacy prototype chain manipulation
obj.__proto__ = parent;
Dynamic prototype assignment not supported. Use Object.create() or class extends.
String.prototype.substr Deprecated substring extraction
"hello".substr(1, 3); // "ell"
// use .slice() or .substring()
Deprecated. Use .slice() which is fully supported.
Octal literals (0777) Legacy octal number syntax
const n = 0777;
const ok = 0o777;
Legacy octal forbidden in strict mode. ES2015 0o prefix works.
escape() / unescape() Deprecated string encoding globals
escape("hello world");
// use encodeURIComponent()
Deprecated globals. Use encodeURIComponent / decodeURIComponent.
Function.prototype.caller Access calling function reference
function f() {
  return f.caller;
}
Forbidden in strict mode. Not available in Wasm.
HTML string methods (.bold(), .anchor()) Deprecated HTML wrapper string methods
"text".bold();   // deprecated
"text".anchor("name");
Annex B legacy methods. Not implemented.
RegExp.$1 static properties Legacy RegExp match result globals
/(\d+)/.exec("abc123");
RegExp.$1; // "123" — deprecated
Legacy static properties. Use match result array instead.
Proposals
Temporal Modern date and time API
const now = Temporal.Now.instant();
const date = Temporal.PlainDate
  .from("2026-04-06");
Stage 3 proposal. Large API surface — not yet in scope.
Decorators Class and method metadata annotations
@logged
class MyClass {
  @bound method() { }
}
Stage 3 proposal. Requires compile-time decorator application.
Pattern matching Structural matching expressions
match (value) {
  when ({ x, y }): return x + y;
  when (String): return value.length;
  default: return 0;
}
Stage 1 proposal. Structural pattern matching not yet in scope.

Lean modules that load and run fast.

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.

Browser & Node.js

Runtime execution speed and module load time relative to plain JavaScript, measured in browser and Node.js environments.


Compiled ahead of time, no interpreter.

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.

JavaScript (0.1 KB gzipped)

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1)
       + fibonacci(n - 2);
}

export function run() {
  return fibonacci(10);
}
app.js
compile

WebAssembly (0.2 KB gzipped)

;; 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

Using DOM and JS APIs

DOM access and browser APIs compile to typed host imports. The compiler generates the bindings — you just write normal JavaScript.

JavaScript (0.1 KB gzipped)

const el = document.createElement("div");
el.textContent = "Hello from Wasm";
document.body.appendChild(el);
dom.js
compile

WebAssembly (0.3 KB gzipped)

(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 and run

// 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.


Project roadmap.

The roadmap is to first establish compatibility with existing code, then expand host support, optimize performance, and strengthen security.