Skip to content

Cross-cutting: convert 'translate returns None → silent skip' failures into loud diagnostics #626

@aallan

Description

@aallan

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    codegenCode generation backendenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions