Origin: Cross-cutting failure-shape pattern observed across #583, #604, #614, and (in the closure-context manifestation of) #615.
The pattern
A WASM translator method (_translate_*, _infer_*, etc.) hits an unsupported case and returns None. The caller propagates the None upward — _translate_index_expr returns None if _infer_index_element_type returned None; translate_block returns None if any sub-statement returned None; etc. Eventually one of two things happens:
- Top-level function:
_compile_fn catches the None and emits an [E602] warning "function body contains unsupported expressions — skipped". The function is silently absent from the output module. Downstream — at runtime — the user sees Error: No exported functions to call (if main was skipped) or a "function not found" trap (if a callee was skipped). The error is decoupled from the cause by an arbitrary distance.
- Closure body:
_compile_lifted_closure returns None. But the call site already emitted call_indirect to a registered closure_id, so the WASM module has a call_indirect referencing a function-table entry that was never added. WASM validation rejects the module at instantiation: unknown table 0: table index out of bounds at offset N. No source-level location, no Vera-level diagnostic.
Either way, the failure is loud at runtime but silent during translation — and the [E602] warning channel is currently the only signal, mixed in with success output.
Concrete bugs that hit this shape
| Issue |
Trigger |
Surface |
| #583 |
Type alias over Array<T> in let-binding position |
Silent E602 skip |
| #604 |
5 prelude combinators (option_map/etc.) — bare type-var params + apply_fn in match arms |
Silent E602 skip × 5 every compile |
| #614 |
f()[i] where f returns Array<T> — FnCall collection in IndexExpr |
Top-level: silent E602 skip; closure: WASM validation trap |
| #615 |
Non-contiguous outer-slot capture in closure |
Closure dropped → WASM validation trap |
The category is wider than the individual instances — every return None in vera/wasm/* and vera/codegen/* is a potential silent-skip site.
Proposed: convert silent failures to loud failures
Three layers of intervention, increasing in invasiveness:
Layer 1 — pre-commit gate on the [E602] warning channel
Currently _compile_fn emits the "function body contains unsupported expressions — skipped" warning to the diagnostic stream but compilation succeeds. Add a check-script (scripts/check_e602_clean.py) or extend check_examples.py / check_conformance.py to fail when any example or conformance program triggers an [E602] skip. Cheap to implement; catches the existing-program case immediately.
The 5 #604 combinators are currently expected to skip — would need an explicit allowlist for those entries until #604 is fixed, then the allowlist gets emptied.
Layer 2 — fail-fast in closure-lift
When _compile_lifted_closure returns None, the closure_id has already been registered at the call site. Track unrealised closure_ids and either (a) skip the parent function with an [E602] (consistent with the top-level path) or (b) raise a structured codegen error that names the specific AST node that failed translation. Either way: no malformed call_indirect escapes to WASM validation.
Layer 3 — replace return None with raised diagnostics
Audit every return None in vera/wasm/ and vera/codegen/ (probably 50+ sites). For each, decide:
- Genuine "this isn't supported, skip" → keep but raise a
CodegenSkip that carries the AST node + reason. _compile_fn catches and emits a much richer [E602] than today's "unsupported expressions".
- "This shouldn't happen if type-check passed" → raise a
CodegenInvariantError. No silent fall-through; explicit assertion of the invariant.
- "This is a known gap and we have a fallback" → keep the None but document it, and verify the fallback path actually works.
Layer 3 is the biggest commit but turns the failure-shape from "implicit, location-free" into "explicit, AST-node-tagged". Same energy budget as #597 (walker-completeness audit).
Pairs with #597
#597 covers the static dispatch-chain coverage problem: a walker handler omitting branches for some AST subclasses. This issue covers the runtime failure-handling problem: a handler that did run but couldn't complete returned None, and the caller silently degraded. Different audit, different fix surface, complementary outcome — together they close the "translation silently no-ops" failure-shape category.
Trigger condition for promotion
Promotes from "tracking issue" to "active stabilisation work" once one more bug of the same shape is filed (or once #604 is in flight, since it's the cleanest concrete instance). Layer 1 alone is independently shippable as a pre-commit hardening — could be filed as a sub-issue if this is going to sit in the queue.
Origin
Pattern observed during #625 closing #614 + #615. Both bugs were instances of this cross-cutting class; closing them individually doesn't close the next one.
Origin: Cross-cutting failure-shape pattern observed across #583, #604, #614, and (in the closure-context manifestation of) #615.
The pattern
A WASM translator method (
_translate_*,_infer_*, etc.) hits an unsupported case and returnsNone. The caller propagates the None upward —_translate_index_exprreturns None if_infer_index_element_typereturned None;translate_blockreturns None if any sub-statement returned None; etc. Eventually one of two things happens:_compile_fncatches the None and emits an[E602]warning "function body contains unsupported expressions — skipped". The function is silently absent from the output module. Downstream — at runtime — the user seesError: No exported functions to call(if main was skipped) or a "function not found" trap (if a callee was skipped). The error is decoupled from the cause by an arbitrary distance._compile_lifted_closurereturns None. But the call site already emittedcall_indirectto a registered closure_id, so the WASM module has acall_indirectreferencing a function-table entry that was never added. WASM validation rejects the module at instantiation:unknown table 0: table index out of bounds at offset N. No source-level location, no Vera-level diagnostic.Either way, the failure is loud at runtime but silent during translation — and the [E602] warning channel is currently the only signal, mixed in with success output.
Concrete bugs that hit this shape
Array<T>in let-binding positionoption_map/etc.) — bare type-var params +apply_fnin match armsf()[i]wherefreturnsArray<T>— FnCall collection in IndexExprThe category is wider than the individual instances — every
return Noneinvera/wasm/*andvera/codegen/*is a potential silent-skip site.Proposed: convert silent failures to loud failures
Three layers of intervention, increasing in invasiveness:
Layer 1 — pre-commit gate on the
[E602]warning channelCurrently
_compile_fnemits the "function body contains unsupported expressions — skipped" warning to the diagnostic stream but compilation succeeds. Add a check-script (scripts/check_e602_clean.py) or extendcheck_examples.py/check_conformance.pyto fail when any example or conformance program triggers an[E602]skip. Cheap to implement; catches the existing-program case immediately.The 5 #604 combinators are currently expected to skip — would need an explicit allowlist for those entries until #604 is fixed, then the allowlist gets emptied.
Layer 2 — fail-fast in closure-lift
When
_compile_lifted_closurereturns None, the closure_id has already been registered at the call site. Track unrealised closure_ids and either (a) skip the parent function with an [E602] (consistent with the top-level path) or (b) raise a structured codegen error that names the specific AST node that failed translation. Either way: no malformedcall_indirectescapes to WASM validation.Layer 3 — replace
return Nonewith raised diagnosticsAudit every
return Noneinvera/wasm/andvera/codegen/(probably 50+ sites). For each, decide:CodegenSkipthat carries the AST node + reason._compile_fncatches and emits a much richer[E602]than today's "unsupported expressions".CodegenInvariantError. No silent fall-through; explicit assertion of the invariant.Layer 3 is the biggest commit but turns the failure-shape from "implicit, location-free" into "explicit, AST-node-tagged". Same energy budget as #597 (walker-completeness audit).
Pairs with #597
#597 covers the static dispatch-chain coverage problem: a walker handler omitting branches for some AST subclasses. This issue covers the runtime failure-handling problem: a handler that did run but couldn't complete returned None, and the caller silently degraded. Different audit, different fix surface, complementary outcome — together they close the "translation silently no-ops" failure-shape category.
Trigger condition for promotion
Promotes from "tracking issue" to "active stabilisation work" once one more bug of the same shape is filed (or once #604 is in flight, since it's the cleanest concrete instance). Layer 1 alone is independently shippable as a pre-commit hardening — could be filed as a sub-issue if this is going to sit in the queue.
Origin
Pattern observed during #625 closing #614 + #615. Both bugs were instances of this cross-cutting class; closing them individually doesn't close the next one.