Skip to content

Five prelude combinators silently skipped from every WASM compile (option_map / option_and_then / result_map + two _unwrap_or variants) #604

@aallan

Description

@aallan

Investigation: Five prelude combinators are silently skipped from every WASM compile

Component: WASM backend (codegen) + standard prelude
Severity: Medium — silent feature gap, not a crash
Vera version: 0.0.138 (git 20f3ccb)
Found via: vera compile --target browser on any program

Note on framing

In an earlier conversation I dismissed the warning about result_map being
"skipped" as irrelevant prelude noise that didn't affect the program I was
compiling. That dismissal was technically correct for that program but was
the wrong inference about the underlying state of the compiler. On a closer
look, five prelude functions are skipped from every WASM compile, and
the reasons split into two distinct backend gaps. This is worth a separate
look — both as a real feature gap for anyone writing browser-target Vera that
does monadic Option/Result work, and as a quality signal about how much of
the standard prelude actually round-trips through the WASM backend.

Reproducer

Any non-trivial Vera program will surface these. Even the minimum reproducer
from the string-interpolation report (hello\n printer, 14 lines) emits
five [E602] warnings during browser compile:

$ vera compile --target browser hello.vera -o /tmp/out/
  Function 'option_unwrap_or' has unsupported parameter type — skipped.
warning: [E602] Warning at hello.vera, line 20, column 9:

  Function 'option_map' body contains unsupported expressions — skipped.

  The WASM backend does not yet support all Vera expression types. This
  function will not appear in the compiled output.

  Function 'option_and_then' body contains unsupported expressions — skipped.
  Function 'result_unwrap_or' has unsupported parameter type — skipped.
  Function 'result_map' body contains unsupported expressions — skipped.

(The line/column numbers point into the user's file, not the prelude, which
made the warnings feel like noise about user code rather than about the
prelude itself. That framing also probably contributed to my initial dismissal.)

The five affected functions

Definitions extracted verbatim from vera/prelude.py:

private forall<T> fn option_unwrap_or(@Option<T>, @T -> @T)
  requires(true) ensures(true) effects(pure)
{
  match @Option<T>.0 { None -> @T.0, Some(@T) -> @T.0 }
}

private forall<A, B> fn option_map(@Option<A>, @OptionMapFn<A, B> -> @Option<B>)
  requires(true) ensures(true) effects(pure)
{
  match @Option<A>.0 {
    None -> None,
    Some(@A) -> Some(apply_fn(@OptionMapFn<A, B>.0, @A.0))
  }
}

private forall<A, B> fn option_and_then(@Option<A>, @OptionBindFn<A, B> -> @Option<B>)
  requires(true) ensures(true) effects(pure)
{
  match @Option<A>.0 {
    None -> None,
    Some(@A) -> apply_fn(@OptionBindFn<A, B>.0, @A.0)
  }
}

private forall<T, E> fn result_unwrap_or(@Result<T, E>, @T -> @T)
  requires(true) ensures(true) effects(pure)
{
  match @Result<T, E>.0 { Ok(@T) -> @T.0, Err(@E) -> @T.0 }
}

private forall<A, B, E> fn result_map(@Result<A, E>, @ResultMapFn<A, B> -> @Result<B, E>)
  requires(true) ensures(true) effects(pure)
{
  match @Result<A, E>.0 {
    Ok(@A) -> Ok(apply_fn(@ResultMapFn<A, B>.0, @A.0)),
    Err(@E) -> Err(@E.0)
  }
}

Two distinct failure modes

Function Failure Likely cause
option_unwrap_or parameter type bare type variable @T as a parameter
result_unwrap_or parameter type bare type variables @T, @E as parameters
option_map body apply_fn call inside a match arm
option_and_then body apply_fn call inside a match arm
result_map body apply_fn call inside a match arm

The split between the columns is clean. The two _unwrap_or variants don't
contain apply_fn and don't get a "body contains unsupported expressions"
warning — they fail earlier, on the parameter type. The signatures are the
distinguishing feature: _unwrap_or takes a bare @T as its second
argument, whereas _map / _and_then take a concrete generic wrapper type
(@OptionMapFn<A, B> etc.) which the WASM backend can apparently handle as a
parameter.

So the two gaps are:

  1. Bare type variable in WASM parameter position.
    forall<T> fn(@Option<T>, @T -> @T) cannot be lowered because @T on its
    own has no monomorphic WASM representation. The WASM backend appears to
    require type variables to appear only inside a known wrapper type with a
    uniform representation (a pointer pair, presumably).

  2. apply_fn codegen. The README in vera/wasm/ actually mentions this
    directly:

    "Missing cases (e.g. IndexExpr, IfExpr, apply_fn calls) return
    None, which cascades to E602 (unsupported expressions)."
    vera/wasm/README.md:643

    Higher-order function application via apply_fn is the mechanism the
    prelude uses to invoke the closure passed to _map / _and_then. Without
    this in the WASM codegen, no map/bind combinator over Option or Result
    will compile.

Why this is more than cosmetic

For Python-target Vera the prelude works in full — these warnings only fire
on --target browser (and presumably any future native target sharing the
WASM codegen). The practical impact:

  • Any browser-target program that wants to do option_map(opt, fn) or
    result_map(res, fn) will silently lose those calls from its WASM module
    and produce a link/runtime failure when the user calls the missing function.
  • Anyone teaching Vera's effect/Result story to LLMs as "use the standard
    prelude combinators" will hit a wall at the browser target.
  • The warnings are emitted but not gated — the build still produces a
    partial WASM bundle with a happy "Browser bundle: ..." message, so it's
    easy to miss them entirely. (I did, on first read.)

Suggested next steps

  1. Decide intent. Are bare type variables and apply_fn planned for
    WASM codegen, or is the prelude being held back from the actual supported
    subset? If the latter, the prelude could be rewritten in a WASM-compatible
    shape (e.g., monomorphised at a few common types like
    option_map_int, option_map_string — ugly, but compilable).
  2. Promote the warnings. If a prelude function is silently dropped, that
    should arguably be an error at compile time for browser target, or at
    least a single highlighted summary line ("5 prelude functions skipped:
    ..."), rather than five interleaved [E602] warnings that look like they're
    pointing at user code.
  3. Document the gap. The browser-target docs (or a section in
    vera/wasm/README.md) should list which prelude items don't survive the
    WASM lowering, so users writing in this target know to avoid them.
  4. Track apply_fn codegen as a single feature. It's mentioned in the
    README as a known gap, but landing it would unblock all three of
    option_map, option_and_then, result_map — high leverage.

What I should have done the first time

When I saw the warning, I confirmed the program I was working on didn't use
those functions and moved on. The right move would have been to surface the
warning to the user with one extra sentence — "your program doesn't depend
on these but the prelude isn't fully compiling, worth flagging" — rather
than calling it "irrelevant noise." Filing this report is the version of
that I should have done the first time round.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions