Quartz v5.25

WASM backend: calling user Void functions produces invalid WASM

Filed: 2026-04-19, during Phase 3 of the CHIP-8 demo (unikernel-site branch). Severity: Blocks any WASM program that calls user-defined Void-returning functions. LLVM backend unaffected.

Repro

Any program where one user function with : Void return type is called from another function (Int or Void) triggers this. No Vec / while / closure needed — the failure is in the call-site code generation for a void callee.

var g_counter = 0

def _inc(): Void
  g_counter = g_counter + 1
end

def main(): Int
  _inc()
  return g_counter
end
/tmp/wasm-build/quartz_wasm --backend wasm --no-cache /tmp/repro.qz -o /tmp/repro.wasm
wasmtime /tmp/repro.wasm
#   WebAssembly translation error
#   Invalid input WebAssembly code at offset 218:
#       type mismatch: expected i64 but nothing on stack

Workaround

Change : Void to : Int and return 0. End-to-end behavior is identical; only the function signature changes. Callers that ignored the return value still ignore it.

def _inc(): Int
  g_counter = g_counter + 1
  return 0
end

Works on both LLVM and WASM backends.

Root cause (hypothesis)

codegen_wasm.qz::_wasm_emit_instr for MIR_CALL likely always emits code assuming the callee returns one i64, pushing a placeholder onto the stack for the caller to consume. When the callee’s WASM signature is () -> () (no result, which is how Void functions lower — see _wasm_mark_void_func at codegen_wasm.qz:1756), the callee’s return doesn’t push, and the next instruction in the caller expects an i64 it never got.

Likely fix: in the MIR_CALL codegen path, look up the callee’s void-ness via wasm_runtime::_wasm_is_void_func(name) and suppress the result-consumer instructions (or emit an i64.const 0 placeholder) when the callee returns void.

The same handler works on LLVM because LLVM void calls don’t produce a value, and the caller’s IR simply doesn’t reference one — LLVM type system enforces this at the IR level. WASM has a looser type discipline and the validator catches the imbalance later.

Impact on CHIP-8 demo

Forced a local workaround: every def _xxx(): Void helper in examples/chip8/chip8.qz was rewritten to def _xxx(): Int with return 0 at the tail. 11 helpers touched. No behavior change.

Once this is fixed in the compiler, the workaround can be reverted or the decision left as-is (Int-returning helpers are harmless).

Validation target

A green wasmtime --invoke chip8_init /tmp/chip8.wasm on a build of examples/chip8/chip8.qz with Void helpers restored.

Spec: spec/qspec/wasm_void_call_spec.qz should be added once the compiler fix lands — a 3-test spec asserting Void↔Int, Void↔Void, and Int↔Void call edges all produce valid WASM that wasmtime can instantiate.