Running Wado on Node via jco
SkillDev toolsTranspile Wado Wasm components to JS with jco and run/benchmark them on Node. Use when running Wado on Node or browsers via jco, debugging jco-transpiled runtime failures, or benchmarking Wado on Node.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Running Wado on Node via jco skill
What this skill tells your AI
The instructions your AI receives, as published by wado-lang/wado in .claude/skills/jco/SKILL.md and read by ahel’s review.
jco (bytecodealliance) transpiles a Wasm component into JS + core Wasm so it runs on a plain Wasm engine (V8/Node, browsers) instead of a Component Model runtime. Wado targets WASI P3; this doc covers what the released jco does for Wado today and what is still blocked.
TL;DR
- Use the released npm jco as a library (
scripts/jco,mise run jco-*). - Compile Wado with
-f no-wide-arithmetic— V8 has no wide-arithmetic proposal, and float formatting /i128emit it. - Run on Node 26+ — stable JSPI, no flag.
- Compute programs work (including float formatting). Filesystem programs run to completion; reading through a preopen is unverified (see below).
- Quick check:
mise run jco-hello-released. Benchmark:mise run jco-bench <program.wado>.
Environment
- Node 26+ required. Node 26 (V8 14.6) ships stable JSPI
(
WebAssembly.Suspending), so no flag is needed; the repo pinsnode = "26"inmise.toml. Node 24 needs--experimental-wasm-jspi; Node 22's older JSPI fails (WebAssembly.Suspendingis not a constructor). /tmppitfall: outside the repo,nodemay resolve to a system Node 22 (mise activation is path-scoped). Run inside the repo, or use the pinned binary's absolute path.- V8 has no wide-arithmetic in any version (no flag exists). This is a V8
gap, not a jco one — handled by
-f no-wide-arithmetic(see below).
Vendor-free pipeline (scripts/jco)
Released @bytecodealliance/jco as a library. transpile-released.mjs is a
plain transpile() — jco's own preview3-shim serves every import a Wado
program makes, and the output links to it through a node_modules symlink the
script writes beside the files.
The shim flushes stdout after run() resolves, so a runner that calls
process.exit on that promise loses the output. Let the event loop drain.
mise tasks:
mise run jco-deps # npm install released jco under scripts/jco
mise run jco-transpile-released foo.wasm [out-dir]
mise run jco-hello-released # compile + transpile + run hello on Node
mise run jco-bench <program.wado> [runs] # compile -f no-wide-arithmetic, transpile, self-time
Released jco status (verified at 1.30.0)
| Capability | Status |
|---|---|
| Transpile (GC component) | ✅ works, wasi:http/service included |
| JSPI | ✅ native (Node 26 no flag; Node 24 needs the flag) |
| Wide-arithmetic component | ❌ transpile rejects it (wide arithmetic support is not enabled); even if forced, V8 rejects the opcode at runtime → use -f no-wide-arithmetic |
| Stdout via stream | ✅ jco's own shim delivers it, flushed after run() resolves |
| Filesystem read stream | ⚠️ no longer deadlocks; reading through a preopen is unverified |
wide-arithmetic (-f no-wide-arithmetic)
Wado emits the Wasm wide-arithmetic proposal (i64.mul_wide_u/s, i64.add128,
i64.sub128) for float formatting (core:prelude/fpfmt.wado) and i128. No
V8 implements it (checked through Node 26; no flag, --wasm-staging no help),
so any component containing those opcodes fails WebAssembly.compile with
invalid numeric opcode: 0xfc16.
-f no-wide-arithmetic lowers them to plain 32-bit-limb i64 sequences in
wado-compiler/src/codegen/emit/wide_arith_downlevel.rs (delete that file once
V8 ships the proposal). The WIR still shows the wide ops; only the final Wasm
changes. Compile every Node-bound Wado program with this flag — a bare
println of a float needs it.
WASI shims
BA ships a preview3-shim implementing P3 cli / clocks / filesystem /
http, with a browser build beside the Node one. A plain jco transpile wires
it, and stdout, float formatting, wasi:random, MonotonicClock and an HTTP
handle all run through it unaided. Its browser cli is unimplemented
(throw new Todo()), which is why the playground keeps a hand-written one.
A hand-written clock is where precision goes missing: system-clock.now is an
instant carrying sub-second nanoseconds, and get-resolution a bare duration.
Benchmarking on Node
mise run jco-bench <program.wado> [runs] compiles with -f no-wide-arithmetic,
transpiles via the released pipeline, and runs the program self-timed runs
times (default 3; keep the best). The benchmark programs already self-time via
core:benchmark + MonotonicClock and print their own
throughput line, so no host timing is needed.
mise run jco-bench benchmark/count_prime/count_prime.wado
Works today (compute-only — Stdout + MonotonicClock):
| Benchmark | Wado on Node (jco) | Wado on wasmtime |
|---|---|---|
| count-prime | ~4.3 M numbers/s | ~4.6 M |
| mandelbrot | ~4.0 M px/s | ~4.2 M |
| sieve | ~150 M numbers/s | ~64 M (V8 ~2.3× faster) |
| fts | ~12 M conv/s | — |
Compute throughput on V8 lands within ~5–10% of wasmtime (sieve is much faster on V8). Numbers are indicative on a noisy cloud VM; keep best-of-3.
Unported (the other benchmarks — Preopens / wasi:filesystem): zlib,
json-{twitter,canada,catalog}, sqlite-parse, syntax-highlight, cbor. They read
input data from preopened files, which the pipeline does not set. The data load
sits outside the timed loop, so wiring preopens runs these unchanged.
Known blockers (jco / V8 gaps)
wide-arithmetic (V8)
Not jco. Handled by -f no-wide-arithmetic.
Filesystem reads (jco)
A file-reading program no longer hangs — example/cat.wado runs to completion
against preview3-shim. What it reads is unconfirmed: the shim needs its
preopens set (_setPreopens in filesystem/descriptor.js), and the
benchmarks that load data from a preopen are still unported.
Reusing an instance (jco)
An instance serves a couple of calls, then the next suspends on a stream read
whose host injection is never driven (JCO_DEBUG=1 ends at
[StreamEnd#copy()] blocked). cloudflare-worker/ builds one per request.
Debugging jco runtime errors
Transpiled output is one large JS file. Useful canonical-builtin → JS mappings:
| Wasm builtin | jco JS function | Notes |
|---|---|---|
stream.write | streamWrite() | JSPI Suspending |
stream.read | streamRead() | JSPI Suspending |
canon lower (async) | _lowerImportBackwardsCompat() | JSPI Suspending |
task.return | taskReturn() | |
future.new (lift) | _genStreamHostInjectFn / createReadableStreamEnd | host→guest wiring |
Techniques
- Catch swallowed errors — jco's async machinery loses errors as unhandled
rejections:
process.on('unhandledRejection', e => { console.error('UNHANDLED:', e); process.exit(1); }); JCO_DEBUG=1 node run.mjs— verbose trace of every instruction/trampoline. A trailing[ComponentAsyncState#suspendTask()]with no progress = a rendezvous deadlock.- Inject logging by string-replacing a function header in the transpiled JS
(e.g. add
console.error(...)tostreamRead/generatedStreamHostInject). - Timeout hangs:
timeout 12 node run.mjsso a deadlock doesn't wedge.
Common error patterns
| Error | Likely cause |
|---|---|
invalid numeric opcode: 0xfc16 | Wide-arithmetic — recompile with -f no-wide-arithmetic |
WebAssembly.Suspending is not a constructor | Node < 24, or Node 22 picked up outside the repo (use Node 26) |
FutureReadableEnd is not defined | Future-end classes not injected (run via transpile-released.mjs) |
| stdout empty | Missing the stream-write hook or cli.js map |
wide arithmetic support is not enabled (transpile) | Compile with -f no-wide-arithmetic; V8 cannot run the opcodes either |
| Hang / timeout | JSPI Suspending missing on a trampoline, or a stream rendezvous deadlock (the filesystem read gap) |
Signals
- GitHub stars
- 113
- Forks
- 2
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
jco- Source
- github.com/wado-lang/wado