Counter — Mere → DOM

A minimal interactive demo for the Mere frontend FFI (Phase 48). The button below is wired to the count through contrib/dom/dom.mere: the click handler is a Mere closure compiled to Wasm, dispatched back from JavaScript via the exported function table.

Source (counter.mere)

import "contrib/dom/dom.mere";

let display = dom_get_by_id "count" in
let btn = dom_get_by_id "tick" in
let counter = vec_new () in
let _ = vec_push counter 0 in
let _ = dom_set_text display "0" in
let _ = dom_on_click btn (fn (u: unit) ->
  let cur = vec_get counter 0 in
  let next = cur + 1 in
  let _ = vec_set counter 0 next in
  dom_set_text display (show next)
) in
0

What's happening

  1. mere -w counter.mere > counter.wat produces WebAssembly text format containing 4 host imports (dom_get_by_id, dom_set_text, dom_on_click, dom_input_value) plus an export of __indirect_function_table.
  2. wat2wasm counter.wat -o counter.wasm assembles the binary (~6KB).
  3. The page below loads counter.wasm and wires the env imports to real DOM operations via contrib/dom/dom.glue.js.
  4. When you click, the Mere closure runs: vec_get / + 1 / vec_set / dom_set_text. The counter state lives in a single slot of vec_new () in the Wasm bump arena, which persists for the page lifetime.

How JS sees the closure

dom_on_click receives the closure as an i32 pointer to a 2-word record { env, fn_idx } in linear memory. The host glue reads both words and dispatches through the exported __indirect_function_table:

// inside dom.glue.js
dom_on_click: (handleIdx, closurePtr) => {
  const view = new Int32Array(memory.buffer);
  const env = view[closurePtr >> 2];
  const fnIdx = view[(closurePtr + 4) >> 2];
  el.addEventListener("click", () =>
    table.get(fnIdx)(env, 0)
  );
},