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:

  • JSundefined /*TODO:<what>*/, reported as [lengc js] <file>: N unsupported node(s).
  • WASM — a unreachable instruction, 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 formsaddr-of-location falls 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. Integer div/mod by zero is in this family: rather than raising DivByZeroDefect it yields a JS Infinity/NaN, which reads back as 0 when stored into an int slot and throws a confusing RangeError if printed directly. Guard divisors yourself.
  • The Number/BigInt boundary is the hardest edge. Under --bits:32, int/uint map to a JS Number (≤32-bit) and only int64/uint64 become a BigInt. Mixing the two in a single JS operation throws TypeError: 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 ArrayBuffer is 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/breakblock/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. A seq/string that grows needs the ported allocator running inside the module (real mmap/atomics host imports carving pages out of linear memory). This is the next substantial piece of work.
  • Indirect / unresolved callswasmcodegen.nim:455 bails 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.


Back to top

aoughwl — next-gen self-hosted platform for things n stuff. Reach out on Discord (timbuktu_guy) for access to the private backends.

This site uses Just the Docs, a documentation theme for Jekyll.