The measurement
Same module (compiler.pas built --target=wasm32), same sandbox, same input,
five runs each:
| host | result |
|---|---|
wasmtime 48.0.1 |
5/5 exit 0, deterministic 68,248-byte ELF, byte-identical to the native compiler's output for the same source |
node v22.22.1 (node:wasi) |
5/5 SIGSEGV (exit 139), artifact 0–3 bytes |
Node is fine on everything lighter, measured:
| invocation | node | wasmtime |
|---|---|---|
--version |
0 | 0 |
--where |
0 | 0 |
--list-libraries (walks a directory: PxxListDir → sysgetdents64 → fd_readdir) |
0 | 0 |
t.pas tout (full compile) |
139 | 0 |
It also dies on the SYNTAX-ERROR path, after printing the correct diagnostic — so it does not need codegen to have succeeded, only for real work to have been done. Non-deterministic in the artifact size across identical runs.
Why this is host-side, with a control this time
A sandboxed wasm guest cannot fault its host; every guest-side memory error is a trap. The predecessor ticket made that argument and was wrong to conclude from it, because node's leniency was hiding a genuine guest defect (an under-aligned u64 WASI out-param) that surfaced only as late host death.
That defect is now fixed and wasmtime — a strict host that refused it — runs the same module cleanly and repeatedly. So the argument now has the control it lacked: the guest is demonstrably well-formed enough for an independent preview1 implementation to execute it to completion, five times.
That is not proof. wasmtime could tolerate a different latent guest defect just as node tolerated the alignment one. The honest statement is that the evidence has moved from "inference from a sandbox property" to "inference plus a passing second implementation", and the way to settle it is a third host or a node build with symbols.
Why it is prio 25 and not 60
The milestone this was blocking — pascal26 runs under wasmtime — is met.
wasmtime is the campaign's host from here, node stays useful as the lenient
comparison every other slice in test/wasm/ runs under. Nothing is blocked on
this; it is a portability data point about one embedder.
Raise it if the campaign ever needs a browser or node target, where V8 is the engine and this stops being optional.
Where to look, if it is picked up
The stack-overflow hypothesis from the predecessor ticket is still the leading
one and still unmeasured: V8 runs wasm frames on the native stack and does not
reliably convert exhaustion into a RangeError from inside wasm, and the
compiler's recursive-descent parser and arena release walk both recurse with
program size — which fits "dies only on the heaviest workload" and "dies on the
error path too".
Cheapest discriminators, in order:
- Bisect by INPUT SIZE. If the crash threshold tracks source complexity rather than any particular construct, depth is the answer.
- Run the guest on a node worker thread with an explicit
resourceLimitsstack and see whether the threshold moves. --stack-size=40000withulimit -s unlimitedwas already tried and does not help, which does NOT settle it (V8 caps its own limit independently).- If it is not depth, the ~117 MB grown memory is the next candidate.
Do not spend long here without a reason: it is one embedder's failure on a module a different embedder runs correctly.