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)
Summary
vera/browser/runtime.mjsexports 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 aStringargument 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:
step(grid) -> gridiseffects(pure)and bit-identical to the terminal versionrequestAnimationFramedriving a<canvas>, callingsteponce per framePer-frame call shape:
call("step_packed", currentStateString). Butcall(fnName, ...args)only accepts numeric primitives — there's no marshalling forString(orArray<T>) arguments. The internal helpers exist (allocStringat line 101 ofruntime.mjs,readStringat line 55,allocArrayOfStringsat 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/1chars to stdout, thengetStdout()once and split on newlines:This works for fixed-length simulations (300 × 1760 chars ≈ 528 KB, computes in ~100 ms) but rules out:
Proposed fix
Export the existing internal helpers from
vera/browser/runtime.mjs:Plus a thin wrapper on
callthat auto-marshals JS strings to(ptr, len)pairs: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.mjsexportsallocString,readString, and acallWithStrings(or equivalent auto-marshalling) wrapperfn(@String -> @String)is callable from JS with a JS string and returns a JS string