Skip to content

Browser runtime: export string-marshalling helpers so JS can pass String args to Vera functions #603

@aallan

Description

@aallan

Summary

vera/browser/runtime.mjs exports 12 surface APIs (init, call, getStdout, getState, etc.) but does not export the internal string-marshalling helpers (allocString, readString, allocArrayOfStrings). This blocks the natural "Vera as pure simulation core, JS as driver" pattern in the browser, because there's no way to pass a String argument from JS into a Vera function call.

Discovery context

Surfaced by an agent writing a browser variant of Conway's Life. The agent identified the natural decomposition:

  • Vera holds the pure simulation: step(grid) -> grid is effects(pure) and bit-identical to the terminal version
  • JS holds the timing and rendering: requestAnimationFrame driving a <canvas>, calling step once per frame

Per-frame call shape: call("step_packed", currentStateString). But call(fnName, ...args) only accepts numeric primitives — there's no marshalling for String (or Array<T>) arguments. The internal helpers exist (allocString at line 101 of runtime.mjs, readString at line 55, allocArrayOfStrings at line 181) but they're file-private.

Workaround the agent used

Compute all 300 generations upfront in Vera, emit each as a packed string of 0/1 chars to stdout, then getStdout() once and split on newlines:

-- Vera side: recursive emit_frames called from main, prints all 300 frames
private fn emit_frames(@Array<Array<Bool>>, @Nat -> @Unit) ...

-- main runs the whole simulation up-front
public fn main(@Unit -> @Unit) ... {
  let @Array<Array<Bool>> = make_grid(80, 22);
  IO.print(string_concat(pack_grid(@Array<Array<Bool>>.0), "\n"));
  emit_frames(step(@Array<Array<Bool>>.0), 300)
}
// JS side: read all frames at once, animate via rAF
await init('module.wasm');
call('main');
const frames = getStdout().split('\n').filter(Boolean);
let i = 0;
function tick() {
  if (i < frames.length) { renderFrame(frames[i++]); requestAnimationFrame(tick); }
}
tick();

This works for fixed-length simulations (300 × 1760 chars ≈ 528 KB, computes in ~100 ms) but rules out:

  • Streaming / interactive simulations where the user can perturb the state between frames
  • Long-running or unbounded simulations that don't fit in memory
  • Per-frame UI feedback where Vera needs to read input back from the page

Proposed fix

Export the existing internal helpers from vera/browser/runtime.mjs:

export function allocString(str) { ... }       // already exists at line 101
export function readString(ptr, len) { ... }    // already exists at line 55
export function allocArrayOfStrings(strs) {...} // already exists at line 181

Plus a thin wrapper on call that auto-marshals JS strings to (ptr, len) pairs:

export function callWithStrings(fnName, ...args) {
  const flat = [];
  for (const a of args) {
    if (typeof a === 'string') {
      const [p, l] = allocString(a);
      flat.push(p, l);
    } else {
      flat.push(a);
    }
  }
  return call(fnName, ...flat);
}

Plus a corresponding return-value reader for String-returning functions.

Why this matters for adoption

The terminal-vs-browser split exposed by Conway's Life is a real boundary in Vera's "write once, run anywhere" model — terminal animations rely on ANSI escapes and IO.sleep (main-thread blocking) that the browser runtime doesn't translate. The right pattern for the browser is "Vera computes pure state transitions, JS drives timing and IO," and this gap is the missing piece for that pattern to be ergonomic.

Documented in SKILL.md browser-runtime section as a follow-up to make the split explicit once the helpers are exported.

Acceptance

  • runtime.mjs exports allocString, readString, and a callWithStrings (or equivalent auto-marshalling) wrapper
  • Round-trip example test: a Vera function fn(@String -> @String) is callable from JS with a JS string and returns a JS string
  • Browser-runtime SKILL section documents the JS-driver pattern with a worked example (Conway's Life is a candidate)

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions