aver-lang 0.14.2

VM and transpiler for Aver, a statically-typed language designed for AI-assisted development
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
// Generated by `aver compile --pack cloudflare` — bootstrap that
// satisfies user.wasm's `aver/*` imports inside the Cloudflare
// Workers JS environment. Edit freely; regenerating will overwrite
// this file.
//
// `--preset cloudflare` compiles with `--target wasm`, which uses
// `wasm-merge` to inline the Aver runtime (alloc, GC, HAMT,
// string/list/vector ops) directly into user.wasm. Cloudflare Workers
// reject `WebAssembly.instantiate(bytes, …)` from arbitrary fetched
// bytes, so the single-bundled shape is the only viable path here —
// browsers / Deno / Bun can keep the thinner `--target edge-wasm`
// shape with a runtime fetched from averlang.dev/runtime/.

import userWasm from "./__WASM_NAME__.wasm";

// Module-scope handles populated when the user instance comes up.
// Host-side helpers reach for `userExports.memory` / `.alloc` /
// `.rt_map_from_list` (the merged runtime exports them through
// the same wasm module). `pending` carries per-request state the
// fetch-bridge host imports read and write.
let userInstance = null;
let userMemory = null;
let userExports = null;
let pending = null;

// Cached typed-array views over guest linear memory. JS `ArrayBuffer`
// objects are detached (not just resized) when wasm calls
// `memory.grow`, so any `DataView` / `Uint8Array` built before the
// grow becomes unusable. We keep `cachedBuffer` as the identity of
// the last buffer we saw; if it differs from `userMemory.buffer`
// the views get rebuilt. Same pattern wasm-bindgen uses for its
// generated shims; saves a `new DataView` + `new Uint8Array` per
// host import on the hot path.
let cachedBuffer = null;
let cachedDataView = null;
let cachedUint8 = null;

function refreshViews() {
  if (cachedBuffer !== userMemory.buffer) {
    cachedBuffer = userMemory.buffer;
    cachedDataView = new DataView(cachedBuffer);
    cachedUint8 = new Uint8Array(cachedBuffer);
  }
}

const encoder = new TextEncoder();
const decoder = new TextDecoder("utf-8");

function writeAverString(text) {
  // Build an OBJ_STRING in guest memory: 8-byte header + UTF-8 bytes.
  // Header low 32 bits = byte length; kind/tag/meta high bytes are
  // naturally zero for plain strings.
  //
  // `encodeInto` writes UTF-8 directly into linear memory, skipping
  // the intermediate `Uint8Array` `encoder.encode()` would allocate.
  // We over-allocate `length * 3` (worst-case UTF-8 for any JS char,
  // surrogate pairs already span two JS chars so the bound is safe)
  // and trust the bump allocator — for our string sizes the unused
  // slack is irrelevant.
  const upper = text.length * 3;
  const ptr = userExports.alloc(upper + 8);
  // Refresh AFTER alloc: the bump allocator may trigger memory.grow
  // which detaches the previous ArrayBuffer; cached views built
  // before the grow would be unusable.
  refreshViews();
  const { written } = encoder.encodeInto(
    text,
    cachedUint8.subarray(ptr + 8, ptr + 8 + upper),
  );
  // Header as two i32 writes — avoids the BigInt round-trip a
  // `setBigUint64` would force. The high 4 bytes (kind/tag/meta)
  // are zero for plain OBJ_STRING; alloc returns un-zeroed bump
  // memory so we explicitly clear them.
  cachedDataView.setUint32(ptr, written, true);
  cachedDataView.setUint32(ptr + 4, 0, true);
  return ptr;
}

function readString(ptr, len) {
  refreshViews();
  return decoder.decode(cachedUint8.subarray(ptr, ptr + len));
}

function makeAverImports() {
  return {
    console_print: (ptr, len) => console.log(readString(ptr, len)),
    console_error: (ptr, len) => console.error(readString(ptr, len)),
    console_warn:  (ptr, len) => console.warn(readString(ptr, len)),
    time_unixMs:   () => BigInt(Date.now()),
    random_int: (lo, hi) => {
      const span = hi - lo + 1n;
      return lo + BigInt(Math.floor(Math.random() * Number(span)));
    },
    random_float: () => Math.random(),
    // ─── Fetch bridge: HTTP request fields & response builder ────
    // Each host import is a lazy crossing — guest pays only for
    // the fields it reads.
    request_method: () => writeAverString(pending?.req?.method ?? "GET"),
    request_url: () => writeAverString(
      pending?.req ? new URL(pending.req.url).pathname : "/",
    ),
    request_query: () => writeAverString(
      pending?.req ? new URL(pending.req.url).search.slice(1) : "",
    ),
    request_body: () => {
      // body() is sync from Aver's POV; if we already buffered it
      // we hand back the OBJ_STRING, else empty. Bootstrap pre-reads
      // the body before invoking the handler.
      return writeAverString(pending?.body ?? "");
    },
    request_headers_load: () => {
      const h = pending?.req?.headers;
      if (!h) return 0;
      return buildHeadersMap(h);
    },
    response_text: (status, ptr, len) => {
      // Stash response data; native Response built post-handler. The
      // headers list is populated by `response_set_header` calls
      // emitted in the HttpResponse construction lowering — those
      // fire before this finalizer so they're already on the pending
      // record by the time we land here.
      const existingHeaders = pending.response?.headers ?? [];
      pending.response = {
        status,
        body: readString(ptr, len),
        headers: existingHeaders,
      };
      // Return a non-zero sentinel so user code that checks the
      // handle for truthiness is happy.
      return 1;
    },
    response_set_header: (namePtr, nameLen, valuePtr, valueLen) => {
      // Append `(name, value)` to the pending Response's headers.
      // We push individual pairs (rather than a Map) so multi-value
      // headers — Set-Cookie, Vary, … — preserve order and don't
      // collide.
      if (!pending.response) pending.response = { headers: [] };
      if (!pending.response.headers) pending.response.headers = [];
      pending.response.headers.push([
        readString(namePtr, nameLen),
        readString(valuePtr, valueLen),
      ]);
    },
    // ─── Env ───────────────────────────────────────────────
    // Workers' `env` is the bag of bindings + secrets passed to
    // `fetch(request, env, ctx)`. We stash it on `pending.env` for
    // the duration of the handler call (see the fetch entrypoint
    // below). Returns -1 (Aver's NONE_SENTINEL) when the name
    // isn't bound, otherwise allocates a guest OBJ_STRING.
    env_get: (namePtr, nameLen) => {
      const name = readString(namePtr, nameLen);
      const value = pending?.env?.[name];
      if (typeof value !== "string") return -1;
      return writeAverString(value);
    },
    // Workers `env` is read-only — `env_set` is a no-op so the
    // typed contract `Env.set: (String, String) -> Unit` still
    // holds without lying about persistence.
    env_set: (_namePtr, _nameLen, _valuePtr, _valueLen) => {},
    // ─── Http (client) ──────────────────────────────────────
    // `Http.<verb>(...)` is a synchronous-looking wasm call that
    // really has to `await fetch(...)` on the host. We bridge the
    // sync↔async gap with the JS Promise Integration (JSPI)
    // proposal: each suspending import is wrapped via
    // `WebAssembly.Suspending(asyncFn)`, and the exported handler
    // is wrapped with `WebAssembly.promising(wasmFn)` so a
    // top-level `await` chains the suspension all the way back to
    // the worker's `fetch` entrypoint.
    //
    // Workers / Deno / Bun ship V8 / SpiderMonkey builds with JSPI
    // enabled; if you target a runtime that doesn't have it,
    // replace these with synchronous stubs that return a transport
    // error.
    //
    // Request headers travel over a host-maintained pending list:
    //   `http_clear_request_headers` resets it before each call,
    //   `http_add_request_header(name, value)` appends an entry
    //    (multi-value friendly via repeated calls),
    //   `http_send(method, url, body, contentType)` consumes the
    //    pending list, executes the fetch, returns
    //    `(status, body, err)`.
    http_clear_request_headers: () => {
      pending.requestHeaders = [];
    },
    http_add_request_header: (namePtr, nameLen, valuePtr, valueLen) => {
      if (!pending.requestHeaders) pending.requestHeaders = [];
      pending.requestHeaders.push([
        readString(namePtr, nameLen),
        readString(valuePtr, valueLen),
      ]);
    },
    http_send: makeSuspendingHttpSend(),
    // ─── Print/Format value ─────────────────────────────────────
    // The compiler imports these unconditionally: `print_value`
    // backs `Console.print(non-string)` and the format-debug paths,
    // `format_value` backs string interpolation when an argument
    // isn't already a String (it returns the (ptr, len) of an
    // OBJ_STRING describing the value). Stub bodies just route
    // primitives to their natural string forms; richer formatting
    // (records, lists with shape) is on the host runtime to expand.
    print_value: (tag, val) => console.log(formatValueForHost(tag, val)),
    format_value: (tag, val) => {
      const text = formatValueForHost(tag, val);
      const ptr = writeAverString(text);
      // Two-result return — fetch-bridge ABI declares (ptr, len).
      // The OBJ_STRING handle's bytes start at ptr+8; the i32 length
      // lives in the header low 32 bits. writeAverString already
      // refreshed cachedDataView so we can read straight from it.
      const len = cachedDataView.getUint32(ptr, true);
      return [ptr + 8, len];
    },
  };
}

// JSPI-suspending implementation of `aver/http_send`. The wasm
// guest calls this synchronously; control suspends here while we
// `await fetch(...)`, then resumes with the (status, body, err)
// triple on the wasm stack. `WebAssembly.Suspending` is supported
// out of the box in Cloudflare Workers / Deno / Bun / modern
// browsers; runtimes without JSPI should swap this for a sync
// stub that returns a transport error.
function makeSuspendingHttpSend() {
  if (typeof WebAssembly.Suspending !== "function") {
    // No JSPI — fall back to a sync stub that always reports a
    // transport failure. Programs branch on `Result.Err` instead
    // of crashing on a missing import.
    return (_m, _ml, _u, _ul, _b, _bl, _c, _cl) => {
      const err = writeAverString(
        "Http.send: host has no JSPI; cannot await fetch synchronously",
      );
      return [0n, 0, 0, err];
    };
  }
  return new WebAssembly.Suspending(async (
    methodPtr, methodLen, urlPtr, urlLen,
    bodyPtr, bodyLen, ctPtr, ctLen,
  ) => {
    const method = readString(methodPtr, methodLen);
    const url = readString(urlPtr, urlLen);
    const body = bodyLen > 0 ? readString(bodyPtr, bodyLen) : null;
    const contentType = ctLen > 0 ? readString(ctPtr, ctLen) : null;

    const init = { method };
    const headers = new Headers();
    if (contentType) headers.set("content-type", contentType);
    for (const [name, value] of pending.requestHeaders ?? []) {
      headers.append(name, value);
    }
    if ([...headers].length > 0) init.headers = headers;
    if (body !== null && method !== "GET" && method !== "HEAD") {
      init.body = body;
    }

    try {
      const resp = await fetch(url, init);
      const respBody = await resp.text();
      const bodyHandle = writeAverString(respBody);
      // Bulk-transfer the upstream response headers into a guest
      // `Map<String, List<String>>` so Aver code can read
      // `resp.headers` (Content-Type, Set-Cookie, rate-limit
      // headers, ETag, …). Empty Map (`0`) when the upstream
      // sent nothing — same shape Aver code already handles.
      const headersHandle = buildHeadersMap(resp.headers);
      return [BigInt(resp.status), bodyHandle, headersHandle, 0];
    } catch (e) {
      const msg = e?.message ?? String(e);
      const errHandle = writeAverString(msg);
      return [0n, 0, 0, errHandle];
    }
  });
}

// Aver runtime kind tags / wrap codes the host needs when
// hand-building heap objects (OBJ_LIST_CONS for cons cells,
// OBJ_TUPLE for (name, values) entries). Mirrors
// `src/codegen/wasm/value.rs`.
const OBJ_LIST_CONS = 4n;
const OBJ_TUPLE = 7n;
const KIND_STR = 3;

// Build an `OBJ_LIST_CONS` cell at a fresh allocation. Layout
// matches `rt_list_cons` in the runtime: 8-byte header + 8-byte
// head value (i64) + 8-byte tail (i32 sign-extended into an i64
// slot).
function consCell(headValue, tail, headPtrFlag) {
  // Header: kind << 56 | head_ptr_flag << 32 | field_count(2).
  // Decompose into two i32 writes — skips the BigInt construction
  // we'd otherwise pay for the kind constant and the metadata.
  // High 4 bytes: kind(8 bits) << 24 | tag(0) << 16 | meta(low 16).
  // Low 4 bytes: field_count.
  const ptr = userExports.alloc(24);
  refreshViews(); // alloc may have triggered memory.grow
  const high = (Number(OBJ_LIST_CONS) << 24) | (headPtrFlag & 0xffff);
  cachedDataView.setUint32(ptr, 2, true);
  cachedDataView.setUint32(ptr + 4, high, true);
  cachedDataView.setBigUint64(ptr + 8, headValue, true);
  cachedDataView.setInt32(ptr + 16, tail | 0, true);
  cachedDataView.setInt32(ptr + 20, 0, true);
  return ptr;
}

// Build an `OBJ_TUPLE` of two heap pointers — used for the
// (name, values_list) entries that `rt_map_from_list` consumes.
function tupleStrList(namePtr, valuesListPtr) {
  // Same i32 split as consCell. Meta = 0b11 (both fields are heap
  // ptrs) lives in the low 16 bits of the high word.
  const ptr = userExports.alloc(24);
  refreshViews(); // alloc may have triggered memory.grow
  const high = (Number(OBJ_TUPLE) << 24) | 0x3;
  cachedDataView.setUint32(ptr, 2, true);
  cachedDataView.setUint32(ptr + 4, high, true);
  cachedDataView.setUint32(ptr + 8, namePtr, true);
  cachedDataView.setUint32(ptr + 12, 0, true);
  cachedDataView.setUint32(ptr + 16, valuesListPtr, true);
  cachedDataView.setUint32(ptr + 20, 0, true);
  return ptr;
}

// Walk a JS `Headers` object and return a guest-side
// `Map<String, List<String>>` handle. Multi-value `Set-Cookie`
// uses `getSetCookie()` (Web Fetch API) so individual cookies
// land as separate list entries; everything else collapses
// into a single-element list (the Headers iterator already
// comma-joins same-name fields per RFC 9110 §5.3, which is the
// shape Aver code receives).
function buildHeadersMap(jsHeaders) {
  if (typeof userExports.rt_map_from_list !== "function") {
    return 0;
  }
  let listTail = 0;

  // Set-Cookie first (if available) so the resulting Map sees
  // a real `["c1", "c2", …]` list under that name, not a
  // comma-collapsed single-element value.
  let cookies = [];
  if (typeof jsHeaders.getSetCookie === "function") {
    cookies = jsHeaders.getSetCookie();
  }
  if (cookies.length > 0) {
    let valueList = 0;
    for (let i = cookies.length - 1; i >= 0; i--) {
      const cookieStr = writeAverString(cookies[i]);
      valueList = consCell(BigInt(cookieStr), valueList, 1);
    }
    const nameStr = writeAverString("set-cookie");
    const tuple = tupleStrList(nameStr, valueList);
    listTail = consCell(BigInt(tuple), listTail, 1);
  }

  // Everything else — Headers iteration yields lowercase names
  // already, so we don't have to normalise. Skip Set-Cookie since
  // we handled it above (its iterator entries are also already
  // collapsed into one comma-joined string by spec, which is wrong
  // for Set-Cookie — `getSetCookie()` is the canonical reader).
  for (const [name, value] of jsHeaders) {
    if (name.toLowerCase() === "set-cookie") continue;
    const nameStr = writeAverString(name);
    const valueStr = writeAverString(value);
    const valueList = consCell(BigInt(valueStr), 0, 1);
    const tuple = tupleStrList(nameStr, valueList);
    listTail = consCell(BigInt(tuple), listTail, 1);
  }

  return userExports.rt_map_from_list(listTail, KIND_STR, 1);
}

// Tag values follow Aver's runtime convention:
//   0 = Int (i64 raw bits in `val`)
//   1 = Float (f64 reinterpreted from `val`)
//   2 = Bool (i32 in low bits)
//   3 = String (OBJ_STRING ptr in low 32 bits — read length + bytes)
//   4 = Heap (record/list/etc. — render generic placeholder for now)
function formatValueForHost(tag, val) {
  switch (tag) {
    case 0: return String(val);            // Int
    case 1: {
      // Float — i64 bits → f64
      const buf = new ArrayBuffer(8);
      new BigInt64Array(buf)[0] = val;
      return String(new Float64Array(buf)[0]);
    }
    case 2: return Number(val) ? "true" : "false";
    case 3: {
      const ptr = Number(val & 0xffffffffn);
      if (ptr === 0) return "";
      refreshViews();
      const len = cachedDataView.getUint32(ptr, true);
      return decoder.decode(cachedUint8.subarray(ptr + 8, ptr + 8 + len));
    }
    default: return "<heap>";
  }
}

async function instantiate() {
  const imports = { aver: makeAverImports() };
  // Wrangler's `CompiledWasm` import gives a `WebAssembly.Module`,
  // not raw bytes — `WebAssembly.instantiate(module, imports)`
  // returns the `Instance` directly (not `{instance, module}`).
  const instance = await WebAssembly.instantiate(userWasm, imports);
  userInstance = instance;
  userExports = instance.exports;
  userMemory = instance.exports.memory;

  // JSPI: when Http.* effects run, the wasm frame can suspend on
  // `await fetch(...)` inside `http_send`. To let the suspension
  // unwind back to a Promise, the entry the host calls must be
  // wrapped with `WebAssembly.promising`. Stash the wrapped
  // handler on a global so the worker's `fetch` entrypoint can
  // call `await averHandle(0)` instead of poking `instance.exports`
  // directly (its `aver_http_handle` slot is read-only).
  if (
    typeof instance.exports.aver_http_handle === "function" &&
    typeof WebAssembly.promising === "function"
  ) {
    globalThis.__aver_http_handle = WebAssembly.promising(
      instance.exports.aver_http_handle,
    );
  } else if (typeof instance.exports.aver_http_handle === "function") {
    // No JSPI on this host — call sync. Programs that hit Http.*
    // get a transport-error fallback from `makeSuspendingHttpSend`.
    globalThis.__aver_http_handle = instance.exports.aver_http_handle;
  }
  return instance;
}

let instancePromise = null;

export default {
  async fetch(request, env, ctx) {
    if (!instancePromise) instancePromise = instantiate();
    const user = await instancePromise;

    // If --handler was passed, the user's HTTP handler is exported
    // as `aver_http_handle`. We pre-read the body, stash request
    // state, invoke the handler, then build a native Response from
    // whatever the handler stashed via `response_text`.
    if (typeof user.exports.aver_http_handle === "function") {
      const body = await request.text();
      pending = { req: request, body, env, response: null, requestHeaders: [] };
      try {
        // `__aver_http_handle` is the JSPI-wrapped entry — `await`
        // it so any `Http.*` suspension chain resolves before we
        // build the Response. Falls back to a sync call when
        // JSPI isn't available (see `instantiate`).
        await globalThis.__aver_http_handle(0);
      } catch (e) {
        console.error("aver_http_handle threw:", e);
        pending = null;
        return new Response("Internal error\n", { status: 500 });
      }
      const resp = pending.response;
      pending = null;
      if (!resp) {
        return new Response(
          "Handler returned without calling Response builder\n",
          { status: 500 },
        );
      }
      // Build the native Headers from the (name, value) pairs the
      // guest produced. Web Fetch `Headers.append` is multi-value
      // safe — duplicate Set-Cookie entries stay distinct, comma-
      // joined headers are emitted as separate appends so the host
      // can decide whether to fold (per RFC 9110 §5.3 it can; for
      // Set-Cookie per RFC 6265 it shouldn't).
      const headers = new Headers();
      for (const [name, value] of resp.headers ?? []) {
        headers.append(name, value);
      }
      // Default content-type only when the guest didn't set one.
      if (!headers.has("content-type")) {
        headers.set("content-type", "text/plain;charset=utf-8");
      }
      return new Response(resp.body, { status: resp.status, headers });
    }

    // No --handler: run _start once for side-effect output, return
    // a placeholder response. Useful for "Hello from Aver" demos
    // that just want to prove the toolchain works.
    if (typeof user.exports._start === "function") {
      user.exports._start();
    } else if (typeof user.exports.main === "function") {
      user.exports.main();
    }
    return new Response("Aver program ran. Output went to wrangler logs.\n", {
      headers: { "content-type": "text/plain;charset=utf-8" },
    });
  },
};