← board

wasm32 emits a local index of -1, so every NilPy module fails validation

Repro, one line

printf 'print("hi")\n' > /tmp/m1.npy
./compiler/pascal26 --target=wasm32 /tmp/m1.npy /tmp/m1.wasm    # prints ok:
wasm-validate /tmp/m1.wasm
  004593c: error: unable to read u32 leb128: local.set local index

Any .npy reproduces it, always at function[439], so it is in pylib rather than in user code. A Pascal WriteLn(42) for wasm32 validates and runs.

The bytes, which are not an inference

At 0x45930:

20 05  41 10  6a  24 00  20 0c  21 ff ff ff ff ff ff ff ff ff 01
                                ^^ local.set
local.get 5 / i32.const 16 / i32.add / global.set 0 / local.get 12 /
local.set <ten-byte LEB>

Ten continuation bytes ending 01 is -1 sign-extended to 64 bits. A u32 LEB may be five bytes, hence "representation too long". The next instruction is 20 ff ff ff ...local.get with the same -1. So a local-index sentinel of -1 reaches the encoder and is written out in BOTH directions rather than refused.

local.get 12 immediately followed by local.set -1 reads like a result slot that was never allocated — something asks a proc for its Result local and gets -1. That last sentence is inference from twenty bytes and should not be built on. The -1 operand is not inference; it is in the file.

Why the gap census is structurally blind to this

The compile SUCCEEDS. ok: is printed, no body is refused, nothing appears in the per-gap coverage report — and the module cannot be instantiated. Every check that stops at "did it build" passes, which is the whole reason this sat behind a wall of front-end failures without ever being counted.

wasm-validate is a different instrument from running the module, and it is the cheaper one: it named the instruction and the offset in one command where wasmtime gave a byte offset and a function index. A validation row on the emitted module would catch this class in general, not just this instance.

Who has it

frankA holds the wasm32 backend and has the full diagnosis by message (2026-09-04). Filed anyway because a message is not a record — see umbrella-wasm-is-a-real-platform. Do not start on it without checking with that session first.

RESOLVED 2026-09-04 by frankA, f01eee6fa — and my inference was wrong

Verified here before closing, on fbc02f487f6f: printf 'print("hi")\n' built for wasm32 now passes wasm-validate and prints hi under wasmtime.

The cause was not what I guessed. This ticket said the byte pattern local.get 12 / local.set -1 "reads like a result slot that was never allocated". It is not a Result slot at all: wasm32 keeps four per-body managed-string scratch locals, allocated on demand and reset to -1 per body, and msval was allocated by the managed-STORE path only. WasmVariantPayload reaches the same materialiser without going through a store, so a body that boxed a string VARIABLE into a Variant and never assigned one used a local that had never been allocated. The other three scratch locals were correct by three DUPLICATED copies of the same if < 0 then triple — the shape that let the fourth drift.

The labelling is the part worth keeping. The -1 operand was stated as a fact read out of the file and the cause as inference from twenty bytes, with "should not be built on" attached. The fact held and the inference did not, which is the only reason the wrong half cost nothing.

Why no existing test caught it, which is not the same as no test trying

All four wasm32 Variant rows (test_cross_variant, _single, _payload_widths, _self_assign) emit a VALID module on the PRE-FIX compiler — measured by frankA on a rebuilt pre-fix binary, not argued. They box string LITERALS, and a literal goes through the string pool and never touches msval. Reaching it needs a string VARIABLE boxed in a body with no preceding store, which no test had. test_cross_variant_boxed_string_no_store is that row now, on all six targets.

The class, which outlived the instance

A compile that SUCCEEDS and prints ok: while emitting an unusable module is invisible to a gap census by construction. Two instruments came out of it: WasmBodyU32 now refuses a negative index and names the body, and the census runs wasm-validate on every module it writes and reports a fourth bucket, "compiled ok, module REJECTED" — refusing to run at all if wasm-validate is absent rather than silently dropping the bucket.

The residual frankA named and did not claim to have fixed: no Makefile recipe validates or loads an emitted .wasm except the rows that RUN one, so this class in a body no row reaches is still invisible.

Log