aver-lang 0.17.3

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
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
;; aver/* → wasi_snapshot_preview1.* translation shim.
;;
;; user.wasm always emits Aver-flavored host imports (aver/console_print,
;; aver/print_value, ...) regardless of how it is later deployed. Under
;; `--bridge wasip1` this module satisfies those imports by re-exporting
;; them and translating each call into a wasi_snapshot_preview1 call.
;; It shares aver_runtime's memory so it can read OBJ_STRING bytes and
;; write iovec scratch in the same address space as user.wasm.
;;
;; Memory layout it uses (already reserved by aver_runtime IO_SCRATCH):
;;   0..7   : iovec  (i32 ptr, i32 len)
;;   8..11  : nwritten count
;;
;; Coverage for now:
;;   - console_print  → fd_write(stdout=1, ...)
;;   - console_error  → fd_write(stderr=2, ...)
;;   - print_value    → tag-dispatched format + fd_write(1)
;;
;; The remaining aver/* effects (console_readLine, args_*, time_*,
;; random_*, math_*, terminal_*, format_value) trap as `unreachable`
;; today — wasm-merge will keep only the imports user.wasm actually
;; references, so simple programs (Console.print, math, etc.) work
;; while the rest is filled in incrementally.

(module
  (import "aver_runtime" "memory" (memory $mem 1))
  (import "aver_runtime" "rt_int_to_str" (func $rt_int_to_str (param i64 i32) (result i32)))
  (import "aver_runtime" "rt_float_to_str" (func $rt_float_to_str (param f64 i32) (result i32)))
  (import "aver_runtime" "rt_alloc" (func $rt_alloc (param i32) (result i32)))
  (import "aver_runtime" "rt_i64_to_str_obj"
    (func $rt_i64_to_str_obj (param i64) (result i32)))
  (import "aver_runtime" "rt_f64_to_str_obj"
    (func $rt_f64_to_str_obj (param f64) (result i32)))
  (import "wasi_snapshot_preview1" "fd_write"
    (func $wasi_fd_write (param i32 i32 i32 i32) (result i32)))
  (import "wasi_snapshot_preview1" "random_get"
    (func $wasi_random_get (param i32 i32) (result i32)))
  (import "wasi_snapshot_preview1" "clock_time_get"
    (func $wasi_clock_time_get (param i32 i64 i32) (result i32)))
  (import "wasi_snapshot_preview1" "environ_sizes_get"
    (func $wasi_environ_sizes_get (param i32 i32) (result i32)))
  (import "wasi_snapshot_preview1" "environ_get"
    (func $wasi_environ_get (param i32 i32) (result i32)))

  ;; "true" / "false" data literals at fixed offsets in shared memory.
  ;; Picked above IO_SCRATCH (128) but below the heap_base bumped on
  ;; user-module instantiate — these are short, immutable UTF-8 bytes.
  (data (i32.const 96) "true")
  (data (i32.const 100) "false")

  ;; Internal helper: write `len` bytes starting at `ptr` to fd. Sets
  ;; up the iovec at offset 0..7 and the nwritten cell at 8..11.
  (func $write_fd (param $fd i32) (param $ptr i32) (param $len i32)
    i32.const 0
    local.get $ptr
    i32.store
    i32.const 0
    local.get $len
    i32.store offset=4
    local.get $fd
    i32.const 0       ;; iovec ptr
    i32.const 1       ;; iovec count
    i32.const 8       ;; nwritten ptr
    call $wasi_fd_write
    drop
  )

  (func $console_print (param $ptr i32) (param $len i32)
    i32.const 1
    local.get $ptr
    local.get $len
    call $write_fd
  )
  (export "console_print" (func $console_print))

  (func $console_error (param $ptr i32) (param $len i32)
    i32.const 2
    local.get $ptr
    local.get $len
    call $write_fd
  )
  (export "console_error" (func $console_error))

  ;; WASI has no separate "warning" stream — fd 2 (stderr) is the
  ;; conventional landing for both warnings and errors. JS hosts
  ;; (workers, deno, bun) route `console.warn` separately, but for
  ;; standalone wasmtime we route through stderr.
  (func $console_warn (param $ptr i32) (param $len i32)
    i32.const 2
    local.get $ptr
    local.get $len
    call $write_fd
  )
  (export "console_warn" (func $console_warn))

  ;; Env.get: walk the WASI environ table looking for `name=…`.
  ;; WASI preview 1 exposes the whole environment as a flat
  ;; (table + buffer) pair via `environ_sizes_get` + `environ_get`.
  ;; Each table slot points at a `KEY=VALUE\0` C string in the
  ;; buffer. We allocate scratch via `rt_alloc` (the bump
  ;; allocator never frees, so this leaks a few hundred bytes per
  ;; call — acceptable for a config lookup that fires once per
  ;; request handler).
  ;;
  ;; Returns -1 (Aver's NONE_SENTINEL) if the name isn't bound;
  ;; otherwise allocates an `OBJ_STRING` for the value and returns
  ;; its pointer. The caller (emit-side `Env.get` lowering) wraps
  ;; that into `Option.Some(_)`.
  (func $env_get (param $name_ptr i32) (param $name_len i32) (result i32)
    (local $sizes_ptr i32)
    (local $env_count i32)
    (local $env_buf_size i32)
    (local $table_ptr i32)
    (local $buf_ptr i32)
    (local $i i32)
    (local $entry_str i32)
    (local $value_start i32)
    (local $value_len i32)

    ;; Scratch: 8 bytes for (count, buf_size) returned by environ_sizes_get.
    i32.const 8
    call $rt_alloc
    local.set $sizes_ptr

    local.get $sizes_ptr
    local.get $sizes_ptr
    i32.const 4
    i32.add
    call $wasi_environ_sizes_get
    drop

    local.get $sizes_ptr
    i32.load
    local.set $env_count
    local.get $sizes_ptr
    i32.const 4
    i32.add
    i32.load
    local.set $env_buf_size

    ;; No env vars at all.
    local.get $env_count
    i32.eqz
    if
      i32.const -1
      return
    end

    ;; Allocate table (count * 4 bytes for u32 ptrs) and buffer.
    local.get $env_count
    i32.const 4
    i32.mul
    call $rt_alloc
    local.set $table_ptr
    local.get $env_buf_size
    call $rt_alloc
    local.set $buf_ptr

    local.get $table_ptr
    local.get $buf_ptr
    call $wasi_environ_get
    drop

    ;; Walk table[0..count].
    i32.const 0
    local.set $i
    block $miss
      loop $next
        local.get $i
        local.get $env_count
        i32.ge_u
        br_if $miss

        ;; entry_str = table[i]
        local.get $table_ptr
        local.get $i
        i32.const 4
        i32.mul
        i32.add
        i32.load
        local.set $entry_str

        ;; Match: entry_str[0..name_len] == name AND entry_str[name_len] == '='
        local.get $entry_str
        local.get $name_ptr
        local.get $name_len
        call $env_match_prefix
        if
          ;; Found — value starts after the '='.
          local.get $entry_str
          local.get $name_len
          i32.add
          i32.const 1
          i32.add
          local.set $value_start

          ;; Length of NUL-terminated value.
          local.get $value_start
          call $cstr_len
          local.set $value_len

          ;; Allocate OBJ_STRING(value_start, value_len) and return.
          local.get $value_start
          local.get $value_len
          call $alloc_obj_string_copy
          return
        end

        local.get $i
        i32.const 1
        i32.add
        local.set $i
        br $next
      end
    end

    ;; Walked the whole table, no match.
    i32.const -1
  )
  (export "env_get" (func $env_get))

  ;; Helper: returns 1 if `entry_str` starts with the `name_len`
  ;; bytes at `name_ptr` followed by `=`, otherwise 0.
  (func $env_match_prefix
        (param $entry_str i32) (param $name_ptr i32) (param $name_len i32)
        (result i32)
    (local $i i32)
    (local $a i32)
    (local $b i32)

    i32.const 0
    local.set $i
    block $mismatch
      loop $cmp
        local.get $i
        local.get $name_len
        i32.ge_u
        br_if 1
        local.get $entry_str
        local.get $i
        i32.add
        i32.load8_u
        local.set $a
        local.get $name_ptr
        local.get $i
        i32.add
        i32.load8_u
        local.set $b
        local.get $a
        local.get $b
        i32.ne
        br_if $mismatch
        local.get $i
        i32.const 1
        i32.add
        local.set $i
        br $cmp
      end
    end
    ;; If we exited via mismatch, $i < name_len → fail.
    local.get $i
    local.get $name_len
    i32.lt_u
    if
      i32.const 0
      return
    end
    ;; Names matched; require '=' separator at entry_str[name_len].
    local.get $entry_str
    local.get $name_len
    i32.add
    i32.load8_u
    i32.const 0x3D ;; '='
    i32.eq
  )

  ;; Helper: length of NUL-terminated C string at `ptr` (excludes NUL).
  (func $cstr_len (param $ptr i32) (result i32)
    (local $i i32)
    i32.const 0
    local.set $i
    block $done
      loop $next
        local.get $ptr
        local.get $i
        i32.add
        i32.load8_u
        i32.eqz
        br_if $done
        local.get $i
        i32.const 1
        i32.add
        local.set $i
        br $next
      end
    end
    local.get $i
  )

  ;; Helper: allocate an OBJ_STRING (kind=0) with a header carrying
  ;; the byte length, then memcpy the bytes from `src` into the
  ;; payload area. Returns the OBJ_STRING handle.
  (func $alloc_obj_string_copy
        (param $src i32) (param $len i32) (result i32)
    (local $obj i32)

    ;; Allocate header (8 bytes) + payload, 8-byte aligned.
    local.get $len
    i32.const 8
    i32.add
    call $rt_alloc
    local.set $obj

    ;; Header: kind=0 (OBJ_STRING) << 56 | len in low 32 bits.
    ;; Kind 0 means high byte stays zero; just store the length as i64.
    local.get $obj
    local.get $len
    i64.extend_i32_u
    i64.store

    ;; Copy `len` bytes from `src` into `obj + 8`.
    local.get $obj
    i32.const 8
    i32.add
    local.get $src
    local.get $len
    memory.copy

    local.get $obj
  )

  ;; WASI preview 1 has no `setenv`; honour the
  ;; `Env.set: (..) -> Unit` contract by accepting the call and
  ;; dropping it. Programs that need persistent env mutation should
  ;; either use a different runtime (Workers `env` is also frozen,
  ;; same shape) or wait for a preview 2 host that exposes write.
  (func $env_set
        (param $name_ptr i32) (param $name_len i32)
        (param $value_ptr i32) (param $value_len i32))
  (export "env_set" (func $env_set))

  ;; Http.* under WASI preview 1: there's no native HTTP client,
  ;; just sockets (`fd_read`/`fd_write`); wasi-http preview 2 isn't
  ;; in mainstream wasmtime yet. The bridge satisfies the import so
  ;; user.wasm links, but every send returns a transport error so
  ;; programs branch through `Result.Err` instead of crashing.
  ;; Real HTTP from `--bridge wasip1` lands in 0.15 once preview 2
  ;; (or a JS host above wasmtime) is available.
  ;;
  ;; Build the error message bytes lazily into a fresh `rt_alloc`
  ;; buffer on every call. The bump allocator never reclaims, so a
  ;; loop hammering `Http.*` will leak ~50 bytes per call — the
  ;; assumption is "if you're calling Http.* under wasi-bridge
  ;; you're hitting a fallback path, not a hot loop". A static
  ;; data offset would be neater but coordinating it with user.wasm's
  ;; interned-literal table at merge time isn't worth the bytes.
  ;; `request_headers_load` under WASI: there's no host-side
  ;; request to read headers from (the WASI bridge runs Aver
  ;; programs as standalone CLIs, not request handlers). Return
  ;; empty Map handle (`0`) — `req.headers` on a CLI program has
  ;; nothing to read from.
  (func $request_headers_load (result i32)
    i32.const 0
  )
  (export "request_headers_load" (func $request_headers_load))

  (func $http_send
        (param $method_ptr i32) (param $method_len i32)
        (param $url_ptr i32)    (param $url_len i32)
        (param $body_ptr i32)   (param $body_len i32)
        (param $ct_ptr i32)     (param $ct_len i32)
        (result i64 i32 i32 i32)
    (local $msg_buf i32)
    (local $obj i32)

    ;; Allocate scratch + write the message bytes one by one. 40 chars
    ;; (no NUL — `alloc_obj_string_copy` records the length in the
    ;; OBJ_STRING header).
    i32.const 40
    call $rt_alloc
    local.set $msg_buf
    local.get $msg_buf i32.const 0  i32.add  i32.const 0x48 i32.store8 ;; H
    local.get $msg_buf i32.const 1  i32.add  i32.const 0x74 i32.store8 ;; t
    local.get $msg_buf i32.const 2  i32.add  i32.const 0x74 i32.store8 ;; t
    local.get $msg_buf i32.const 3  i32.add  i32.const 0x70 i32.store8 ;; p
    local.get $msg_buf i32.const 4  i32.add  i32.const 0x2E i32.store8 ;; .
    local.get $msg_buf i32.const 5  i32.add  i32.const 0x2A i32.store8 ;; *
    local.get $msg_buf i32.const 6  i32.add  i32.const 0x20 i32.store8 ;; (space)
    local.get $msg_buf i32.const 7  i32.add  i32.const 0x6E i32.store8 ;; n
    local.get $msg_buf i32.const 8  i32.add  i32.const 0x6F i32.store8 ;; o
    local.get $msg_buf i32.const 9  i32.add  i32.const 0x74 i32.store8 ;; t
    local.get $msg_buf i32.const 10 i32.add  i32.const 0x20 i32.store8
    local.get $msg_buf i32.const 11 i32.add  i32.const 0x61 i32.store8 ;; a
    local.get $msg_buf i32.const 12 i32.add  i32.const 0x76 i32.store8 ;; v
    local.get $msg_buf i32.const 13 i32.add  i32.const 0x61 i32.store8 ;; a
    local.get $msg_buf i32.const 14 i32.add  i32.const 0x69 i32.store8 ;; i
    local.get $msg_buf i32.const 15 i32.add  i32.const 0x6C i32.store8 ;; l
    local.get $msg_buf i32.const 16 i32.add  i32.const 0x61 i32.store8 ;; a
    local.get $msg_buf i32.const 17 i32.add  i32.const 0x62 i32.store8 ;; b
    local.get $msg_buf i32.const 18 i32.add  i32.const 0x6C i32.store8 ;; l
    local.get $msg_buf i32.const 19 i32.add  i32.const 0x65 i32.store8 ;; e
    local.get $msg_buf i32.const 20 i32.add  i32.const 0x20 i32.store8
    local.get $msg_buf i32.const 21 i32.add  i32.const 0x75 i32.store8 ;; u
    local.get $msg_buf i32.const 22 i32.add  i32.const 0x6E i32.store8 ;; n
    local.get $msg_buf i32.const 23 i32.add  i32.const 0x64 i32.store8 ;; d
    local.get $msg_buf i32.const 24 i32.add  i32.const 0x65 i32.store8 ;; e
    local.get $msg_buf i32.const 25 i32.add  i32.const 0x72 i32.store8 ;; r
    local.get $msg_buf i32.const 26 i32.add  i32.const 0x20 i32.store8
    local.get $msg_buf i32.const 27 i32.add  i32.const 0x2D i32.store8 ;; -
    local.get $msg_buf i32.const 28 i32.add  i32.const 0x2D i32.store8 ;; -
    local.get $msg_buf i32.const 29 i32.add  i32.const 0x62 i32.store8 ;; b
    local.get $msg_buf i32.const 30 i32.add  i32.const 0x72 i32.store8 ;; r
    local.get $msg_buf i32.const 31 i32.add  i32.const 0x69 i32.store8 ;; i
    local.get $msg_buf i32.const 32 i32.add  i32.const 0x64 i32.store8 ;; d
    local.get $msg_buf i32.const 33 i32.add  i32.const 0x67 i32.store8 ;; g
    local.get $msg_buf i32.const 34 i32.add  i32.const 0x65 i32.store8 ;; e
    local.get $msg_buf i32.const 35 i32.add  i32.const 0x20 i32.store8
    local.get $msg_buf i32.const 36 i32.add  i32.const 0x77 i32.store8 ;; w
    local.get $msg_buf i32.const 37 i32.add  i32.const 0x61 i32.store8 ;; a
    local.get $msg_buf i32.const 38 i32.add  i32.const 0x73 i32.store8 ;; s
    local.get $msg_buf i32.const 39 i32.add  i32.const 0x69 i32.store8 ;; i

    i64.const 0
    i32.const 0
    i32.const 0       ;; headers handle = 0 (empty Map)
    local.get $msg_buf
    i32.const 40
    call $alloc_obj_string_copy
  )
  (export "http_send" (func $http_send))

  ;; The pending-request-header list is host-side state on JS hosts;
  ;; the WASI bridge just drops every entry to keep the type
  ;; contracts honoured. No-op clear, no-op add.
  (func $http_clear_request_headers)
  (export "http_clear_request_headers" (func $http_clear_request_headers))

  (func $http_add_request_header
        (param $name_ptr i32) (param $name_len i32)
        (param $value_ptr i32) (param $value_len i32))
  (export "http_add_request_header" (func $http_add_request_header))

  ;; print_value(tag, val): dispatch on tag and write to stdout.
  ;; tag 0 = Int, 1 = Float bits, 2 = Bool, 3 = String ptr,
  ;; 4 = Heap ptr (today rendered as raw bytes via OBJ_STRING shape),
  ;; 5 = Unit (nothing).
  ;;
  ;; Numbers go through aver_runtime's int_to_str / float_to_str which
  ;; format into the runtime IO_INT_BUF / IO_FLOAT_BUF scratch, then
  ;; we extract (pos, len) from the packed return and fd_write that.
  (func $print_value (param $tag i32) (param $val i64)
    (local $packed i32)
    (local $pos i32)
    (local $len i32)
    (local $ptr i32)

    ;; tag == 0 → Int
    local.get $tag
    i32.const 0
    i32.eq
    if
      local.get $val
      i32.const 16    ;; IO_INT_BUF (re-uses runtime scratch)
      call $rt_int_to_str
      local.set $packed

      i32.const 16
      local.get $packed
      i32.const 16
      i32.shr_u
      i32.add
      local.get $packed
      i32.const 0xFFFF
      i32.and
      i32.const 1
      call $write_fd
      return
    end

    ;; tag == 1 → Float
    local.get $tag
    i32.const 1
    i32.eq
    if
      local.get $val
      f64.reinterpret_i64
      i32.const 48    ;; IO_FLOAT_BUF
      call $rt_float_to_str
      local.set $packed

      i32.const 48
      local.get $packed
      i32.const 16
      i32.shr_u
      i32.add
      local.get $packed
      i32.const 0xFFFF
      i32.and
      i32.const 1
      call $write_fd
      return
    end

    ;; tag == 2 → Bool ("true" / "false" data literals)
    local.get $tag
    i32.const 2
    i32.eq
    if
      local.get $val
      i64.eqz
      if
        i32.const 1
        i32.const 100   ;; offset of "false"
        i32.const 5
        call $write_fd
      else
        i32.const 1
        i32.const 96    ;; offset of "true"
        i32.const 4
        call $write_fd
      end
      return
    end

    ;; tag == 3 (String) or tag == 4 (Heap → assume OBJ_STRING)
    ;; Both render via the OBJ_STRING shape: 8-byte header + bytes,
    ;; header low 32 bits = byte length.
    local.get $tag
    i32.const 3
    i32.eq
    local.get $tag
    i32.const 4
    i32.eq
    i32.or
    if
      local.get $val
      i32.wrap_i64
      local.set $ptr

      local.get $ptr
      i64.load
      i64.const 0xFFFFFFFF
      i64.and
      i32.wrap_i64
      local.set $len

      i32.const 1
      local.get $ptr
      i32.const 8
      i32.add
      local.get $len
      call $write_fd
      return
    end

    ;; tag == 5 → Unit, write nothing.
  )
  (export "print_value" (func $print_value))

  ;; format_value(tag, val) -> (ptr, len). Aver host-side this allocs
  ;; a freshly-formatted string in guest memory; under the bridge we
  ;; do the same: pick the right rt formatter, point at the bytes.
  ;;   tag 0 = Int       → i64_to_str_obj → (ptr+8, len_from_header)
  ;;   tag 1 = Float     → f64_to_str_obj → same shape
  ;;   tag 2 = Bool      → "true"/"false" data literal
  ;;   tag 3 = String    → identity (already an OBJ_STRING ptr)
  ;;   tag 4 = Heap      → assume OBJ_STRING shape (best effort)
  ;;   tag 5 = Unit      → empty
  (func $format_value (param $tag i32) (param $val i64) (result i32 i32)
    (local $obj i32)

    local.get $tag
    i32.const 0
    i32.eq
    if
      local.get $val
      call $rt_i64_to_str_obj
      local.set $obj

      local.get $obj
      i32.const 8
      i32.add
      local.get $obj
      i64.load
      i64.const 0xFFFFFFFF
      i64.and
      i32.wrap_i64
      return
    end

    local.get $tag
    i32.const 1
    i32.eq
    if
      local.get $val
      f64.reinterpret_i64
      call $rt_f64_to_str_obj
      local.set $obj

      local.get $obj
      i32.const 8
      i32.add
      local.get $obj
      i64.load
      i64.const 0xFFFFFFFF
      i64.and
      i32.wrap_i64
      return
    end

    local.get $tag
    i32.const 2
    i32.eq
    if
      local.get $val
      i64.eqz
      if
        i32.const 100   ;; "false"
        i32.const 5
        return
      else
        i32.const 96    ;; "true"
        i32.const 4
        return
      end
    end

    ;; tag == 3 or 4 → assume OBJ_STRING ptr in low 32 bits of val.
    local.get $tag
    i32.const 3
    i32.eq
    local.get $tag
    i32.const 4
    i32.eq
    i32.or
    if
      local.get $val
      i32.wrap_i64
      local.set $obj

      local.get $obj
      i32.const 8
      i32.add
      local.get $obj
      i64.load
      i64.const 0xFFFFFFFF
      i64.and
      i32.wrap_i64
      return
    end

    ;; tag == 5 → empty
    i32.const 0
    i32.const 0
  )
  (export "format_value" (func $format_value))

  ;; ─── Random ─────────────────────────────────────────────────────────
  ;; aver/random_int(min: i64, max: i64) -> i64
  ;; Pull 8 random bytes via wasi.random_get into IO_INT_BUF (16..23),
  ;; reduce mod (max - min + 1), shift to [min, max].
  (func $random_int (param $min i64) (param $max i64) (result i64)
    (local $range i64)
    (local $bits i64)

    i32.const 16   ;; IO_INT_BUF
    i32.const 8
    call $wasi_random_get
    drop

    i32.const 16
    i64.load
    local.set $bits

    local.get $max
    local.get $min
    i64.sub
    i64.const 1
    i64.add
    local.set $range

    local.get $range
    i64.eqz
    if (result i64)
      local.get $min
    else
      local.get $min
      local.get $bits
      local.get $range
      i64.rem_u
      i64.add
    end
  )
  (export "random_int" (func $random_int))

  ;; aver/random_float() -> f64 in [0.0, 1.0)
  ;; Pull 8 random bytes, drop top 11 bits (so 53-bit mantissa fits),
  ;; divide by 2^53.
  (func $random_float (result f64)
    i32.const 16
    i32.const 8
    call $wasi_random_get
    drop

    i32.const 16
    i64.load
    i64.const 11
    i64.shr_u
    f64.convert_i64_u
    f64.const 9007199254740992  ;; 2^53
    f64.div
  )
  (export "random_float" (func $random_float))

  ;; ─── Time ───────────────────────────────────────────────────────────
  ;; aver/time_unixMs() -> i64 (milliseconds since Unix epoch)
  ;; wasi.clock_time_get(clockid, precision, *out_time) returns errno.
  ;; Clock id 0 = REALTIME. Result is nanoseconds; divide by 1e6 → ms.
  (func $time_unixMs (result i64)
    i32.const 0               ;; clock id: REALTIME
    i64.const 1000000         ;; precision (ns) — 1ms is plenty
    i32.const 16              ;; *time_ptr (write into IO_INT_BUF)
    call $wasi_clock_time_get
    drop

    i32.const 16
    i64.load
    i64.const 1000000
    i64.div_u
  )
  (export "time_unixMs" (func $time_unixMs))

)