Capabilities & limitations
An honest map of what each backend does today. Both are experimental; the JavaScript backend is mature for synchronous Nim, the WebAssembly backend runs real programs but not yet heap-growing or higher-order ones.
Graceful degradation, not hard failure
Neither backend aborts on an IR node it can’t lower. It emits a placeholder and counts it:
- JS —
undefined /*TODO:<what>*/, reported as[lengc js] <file>: N unsupported node(s). - WASM — a
unreachableinstruction, reported as[lengc wasm] <file>: N unsupported node(s).
Both test suites compile with zero such placeholders. A non-zero count means you’ve stepped outside the supported surface below — the program may still emit, but that path is a stub.
JavaScript backend — tests/jsbackend, 31/31
Mature for its scope. Verified end-to-end under Node:
| Area | Status |
|---|---|
| Integer arithmetic + 32-bit wrapping | ✅ (Math.imul/\| 0/>>> 0, see binTyped) |
| Floats, bools, chars, conversions | ✅ |
Control flow (if/case/while/break) | ✅ |
| Objects & variant objects | ✅ |
seq, string, Table, HashSet | ✅ |
strutils, sequtils, options | ✅ |
| Closures & proc values | ✅ (dispatched via _fns[idx]) |
| Exceptions | ✅ |
| Garbage collector | ✅ (tgc) |
cstring and FFI (both directions) | ✅ |
DOM (document, events, classList) | ✅ (tdom drives real jsdom) |
Not supported yet:
async/await— no codegen path.- Threads /
spawn— no codegen path. - A few
addr-of forms —addr-of-locationfalls to a placeholder (jscodegen.nim:798).
For ordinary synchronous Nim, the JS backend is effectively feature-complete.
Rough edges to know about (all real, none blockers for typical synchronous code, but each can bite):
- No runtime checks are emitted. Overflow, bounds, and nil checks are not generated by the JS backend — this is intentional for the JS target, but it means a program that relies on those checks raising (
IndexDefect,OverflowDefect, a nil dereference) will not get an exception; it silently proceeds instead. Integerdiv/modby zero is in this family: rather than raisingDivByZeroDefectit yields a JSInfinity/NaN, which reads back as0when stored into an int slot and throws a confusingRangeErrorif printed directly. Guard divisors yourself. - The Number/BigInt boundary is the hardest edge. Under
--bits:32,int/uintmap to a JSNumber(≤32-bit) and onlyint64/uint64become aBigInt. Mixing the two in a single JS operation throwsTypeError: Cannot mix BigInt and other types. Most heavy-64-bit stdlib code (e.g.std/random) can still hit this at internal memory-load / xor sites even when your own code looks clean. This is a known rough edge, not a settled contract. - No stdin or file I/O. The runtime is linear-memory + console only —
echo/stdout works; reading stdin or files does not. - 64 MiB heap cap. The heap
ArrayBufferis fixed at 64 MiB and does not grow on the JS side currently. Allocation past it fails. - Stdlib breadth is not exhaustive. Many modules work (
Table,HashSet,strutils,sequtils,options, and more); breadth beyond the tested set is not guaranteed. Note that some modules fail in the nimony frontend (not this backend) — e.g.json,times,envvars— and so never reach codegen at all.
WebAssembly backend — tests/wasmbackend, 5/5
Real but an early slice. Every module is validated by Node’s WebAssembly engine before running, so malformed output fails loudly rather than limping.
| Area | Status |
|---|---|
| Scalar arith/cmp/bitops/conversions (i32/i64/f32/f64) | ✅ (WASM wraps natively — no Math.imul dance) |
| Structured control flow | ✅ (if/while/break → block/loop/if + br) |
| Direct calls, transitive & cross-module | ✅ |
| Field / array / pointer load-store | ✅ (at shared jslayout offsets — tmemory proves parity with JS) |
| Aggregate construction (objects/arrays) | ✅ (bump-allocated $hp, value copies via memory.copy) |
| Module-level globals | ✅ (fixed memory slots) |
| Constant string data | ✅ (WASM data segments) |
Whole-program mode (--program) | ✅ (emits C main + its closure) |
echo | ✅ (fwrite/fputc/fprintf host imports) |
Not supported yet:
- Heap growth — storage is a bump pointer (
$hp) that only grows and never frees. Aseq/stringthat grows needs the ported allocator running inside the module (realmmap/atomics host imports carving pages out of linear memory). This is the next substantial piece of work. - Indirect / unresolved calls —
wasmcodegen.nim:455bails to a placeholder, so function pointers, closures, and dynamic dispatch don’t work. - Garbage collection / reclamation — none; only the bump region.
So the WASM backend runs straight-line, aggregate, and multi-module programs (including echo "hello world" and echo 42 end to end), but not ones that grow the heap or make higher-order calls.
Shared ground
Both backends compile to a 32-bit platform (--bits:32): int/uint are a JS Number / WASM i32; int64/uint64 are a JS BigInt / WASM i64. Both compute object layout, array strides, and string headers from the single src/jslayout.nim engine, so a Point’s fields land at identical offsets on either target — the memory model is one machine described twice.
Why the split in maturity
The JavaScript backend came first and is the reference. WebAssembly was added as a second instruction selector over the same layout engine and module loader — additive, not a rewrite — which is why its scalar/aggregate/linking story landed quickly but the runtime-dependent pieces (heap, indirect calls) trail behind. See the top-level README for the “one memory model, compiled twice” design.