Summary
apply_fn(closure, ()) where the closure has signature (Unit -> X) trips a WASM type-mismatch at compile time:
type mismatch: expected i64, found i32
The compiled artifact never makes it past WASM validation. vera check passes (the program is well-typed at the source level); the failure is purely in codegen.
Discovered while smoke-testing the v0.0.119 closure-capture bugs (#514 / #535) on v0.0.135 to confirm they were closed. Same bug reproduces with no captures at all — it's not a heap-pointer issue.
Reproducer
type UnitToInt = fn(Unit -> Int) effects(pure);
private fn make_const(@Unit -> @UnitToInt)
requires(true) ensures(true) effects(pure)
{
fn(@Unit -> @Int) effects(pure) { 42 }
}
public fn main(@Unit -> @Int)
requires(true) ensures(true) effects(pure)
{
let @UnitToInt = make_const(());
apply_fn(@UnitToInt.0, ())
}
vera check → OK
vera run:
Error: Unhandled WASM trap: failed to compile: wasm[0]::function[3]::main
Caused by:
0: WebAssembly translation error
1: Invalid input WebAssembly code at offset 993: type mismatch: expected i64, found i32
Root cause
_translate_apply_fn in vera/wasm/closures.py (line ~148) builds the call_indirect type signature from the value-args' inferred WASM types. Per-arg:
wt = self._infer_expr_wasm_type(arg)
if wt == "i32_pair":
arg_wasm_types.extend(["i32", "i32"])
else:
arg_wasm_types.append(wt or "i64") # default to i64
For a UnitLit argument:
translate_expr(UnitLit, env) returns [] — no WASM value pushed (correct)
_infer_expr_wasm_type(UnitLit) returns None (correct — Unit has no representation)
wt or "i64" evaluates to "i64" — a phantom param is registered in the call_indirect signature
The closure-lift side (vera/codegen/closures.py:85) correctly skips Unit params:
elif pwt and pwt != "unsupported":
param_wasm.append(pwt)
So the closure is registered with (param i32) (result i64) (env-only), but the apply_fn site emits a call_indirect of type (param i32) (param i64) (result i64). The validator sees the func_table_idx (an i32) where it expected the phantom i64 value-arg.
WAT diff (boiled down):
(type $closure_sig_0 (func (param i32) (result i64))) ;; closure registered correctly
(type $closure_sig_1 (func (param i32) (param i64) (result i64))) ;; phantom
(func $main
...
call_indirect (type $closure_sig_1) ;; <-- uses sig_1, but stack only has [env, fn_idx]
)
Same falsy-pitfall pattern as #584's _fn_ret_types filter — None was overloaded between "Unit" (skip) and "unknown" (fall back to default).
Fix
vera/wasm/closures.py::_translate_apply_fn — change the per-arg branch to skip Unit args from the signature instead of defaulting to i64:
wt = self._infer_expr_wasm_type(arg)
if wt == "i32_pair":
arg_wasm_types.extend(["i32", "i32"])
elif wt is not None:
arg_wasm_types.append(wt)
Three lines of change.
Acceptance
- The reproducer above runs cleanly post-fix and prints
42.
- New conformance test in
tests/conformance/ covering Unit-arg closure with and without captures.
- Existing
ch05_closures.vera and ch05_nested_closures.vera keep passing.
- All existing tests remain green.
Workaround
Until fixed: take an Int parameter (or any non-Unit type) instead of Unit:
type IntToInt = fn(Int -> Int) effects(pure);
private fn make_const(@Int -> @IntToInt) ...
{
fn(@Int -> @Int) effects(pure) { 42 }
}
...
apply_fn(@IntToInt.0, 0) -- pass a dummy Int
Mechanical but distorts the signature.
Summary
apply_fn(closure, ())where the closure has signature(Unit -> X)trips a WASM type-mismatch at compile time:The compiled artifact never makes it past WASM validation.
vera checkpasses (the program is well-typed at the source level); the failure is purely in codegen.Discovered while smoke-testing the v0.0.119 closure-capture bugs (#514 / #535) on v0.0.135 to confirm they were closed. Same bug reproduces with no captures at all — it's not a heap-pointer issue.
Reproducer
vera check→ OKvera run:Root cause
_translate_apply_fninvera/wasm/closures.py(line ~148) builds the call_indirect type signature from the value-args' inferred WASM types. Per-arg:For a
UnitLitargument:translate_expr(UnitLit, env)returns[]— no WASM value pushed (correct)_infer_expr_wasm_type(UnitLit)returnsNone(correct — Unit has no representation)wt or "i64"evaluates to"i64"— a phantom param is registered in the call_indirect signatureThe closure-lift side (
vera/codegen/closures.py:85) correctly skips Unit params:So the closure is registered with
(param i32) (result i64)(env-only), but the apply_fn site emits acall_indirectof type(param i32) (param i64) (result i64). The validator sees thefunc_table_idx(ani32) where it expected the phantom i64 value-arg.WAT diff (boiled down):
Same falsy-pitfall pattern as #584's
_fn_ret_typesfilter —Nonewas overloaded between "Unit" (skip) and "unknown" (fall back to default).Fix
vera/wasm/closures.py::_translate_apply_fn— change the per-arg branch to skip Unit args from the signature instead of defaulting to i64:Three lines of change.
Acceptance
42.tests/conformance/covering Unit-arg closure with and without captures.ch05_closures.veraandch05_nested_closures.verakeep passing.Workaround
Until fixed: take an
Intparameter (or any non-Unit type) instead ofUnit:Mechanical but distorts the signature.