The-Beginning

MicroWAT 2 + wasm-kernels

WASM-Kernel’s assembler, grown up into something any Xinu app can paste in.

WASM-Kernel (March 2026, in ../wasm-kernel/) had a WebAssembly-text assembler written in plain JavaScript. This folder pulls that idea out on its own, with its bugs fixed and SIMD added, so a single-file app can carry hand-written WebAssembly as text and assemble it when the page loads. There’s no toolchain, no binary blobs, and no dependencies.

file what
microwat.js the assembler, ~19 KB. MicroWAT.assemble(watText) → Uint8Array
kernels.js first kernels: matrix × vector for float32 and ternary (−1/0/+1, int8) weights, each as plain JS, scalar wasm and SIMD wasm
bench.html / bench-node.js the benchmark, in a browser or in Node
experiments/ t8-variants.js: four ways to do the ternary kernel, checked and timed
tests/ vs-wabt.js (byte-for-byte against the reference assembler) and kernels.js (every version against JS)

Is it right?

The assembler: for the two kernel modules plus six syntax test cases, MicroWAT’s output is byte-identical to wabt’s (wabt is WebAssembly’s reference toolkit). That includes the exact case that broke WASM-Kernel 1.0.

node tests/vs-wabt.js     # needs `npm i wabt` (test-only; the library itself has no dependencies)
→ ALL BYTE-IDENTICAL TO WABT

The kernels: across seven shapes, including empty ones, scalar wasm matches the JS reference bit for bit. SIMD matches within float rounding (max relative error 6 × 10⁻⁶ at 1024 columns), because it adds in a different order, as any SIMD sum does.

node tests/kernels.js
→ ALL KERNELS AGREE

Is it faster?

Median time per call; speed-up relative to plain JS (js natural: a float64 accumulator, the way you’d normally write it). js exact-f32 rounds every step to float32 so it matches wasm bit for bit, which is what it costs to make JS deterministic in the same way.

Chromium 153

kernel size plain JS scalar wasm SIMD wasm
float32 256×256 0.333 ms 0.079 ms (4.3×) 0.017 ms (20×)
float32 1024×1024 6.276 ms 1.316 ms (4.8×) 0.318 ms (20×)
float32 4096×1024 24.51 ms 5.510 ms (4.4×) 2.440 ms (10×)
ternary 256×256 0.153 ms 0.115 ms (1.3×) 0.014 ms (11×)
ternary 1024×1024 2.058 ms 1.221 ms (1.7×) 0.218 ms (9.4×)
ternary 4096×1024 9.250 ms 6.310 ms (1.5×) 0.910 ms (10×)

Node 22 (V8)

kernel size plain JS scalar wasm SIMD wasm
float32 256×256 0.156 ms 0.096 ms (1.6×) 0.027 ms (5.9×)
float32 1024×1024 2.671 ms 1.351 ms (2.0×) 0.318 ms (8.4×)
float32 4096×1024 11.63 ms 5.401 ms (2.2×) 2.025 ms (5.7×)
ternary 256×256 0.167 ms 0.115 ms (1.5×) 0.013 ms (13×)
ternary 1024×1024 2.661 ms 1.903 ms (1.4×) 0.210 ms (13×)
ternary 4096×1024 12.32 ms 4.535 ms (2.7×) 1.334 ms (9.2×)

How to read it:

Measured November 4, 2026 in a 2-core cloud container (Intel Xeon @ 2.8 GHz). Your machine will give different numbers; open bench.html and press Run. Raw results are in results/.

Using it

<script src="microwat.js"></script>
<script src="kernels.js"></script>
<script>
(async () => {
  const K = await WasmKernels.create();          // picks SIMD > scalar wasm > JS automatically
  const W = K.alloc('i8', rows * cols), x = K.alloc('f32', cols), y = K.alloc('f32', rows);
  W.view.set(myTernaryWeights); x.view.set(myInput);
  K.matvec_t8(W, x, y, rows, cols);              // y.view now holds the result
})();
</script>

Arrays live inside the wasm memory, so nothing is copied per call. For a single-file app, paste both scripts inline, with a provenance line on top so the copy can be traced when it ages:

/* from The-Beginning/microwat: microwat.js 2.0.0 + kernels.js 0.2.0 (commit <hash>) */

Every inlined copy is a frozen snapshot. The line is how anyone later finds out which version a project carries.

Where this could go

Kehai Interim, November 4, 2026. Built on the assembler in Kham’s WASM-Kernel.