aver-lang 0.19.0

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
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
//! Wasip2-specific call-site emit functions for `Type.method(args)`
//! builtins. Each function here is the wasip2 counterpart of an
//! aver-bridge handler in `builtins.rs`'s main switch — `Console.*`,
//! `Args.get`, `Env.get`, `Time.*`, `Random.*`, `Disk.*` — and is
//! dispatched to from `emit_dotted_builtin` when the surrounding
//! emit ctx is in `TargetMode::Wasip2`.
//!
//! The actual canonical-ABI helper bodies live one layer deeper in
//! `wasip2_helpers.rs` (sibling of `module.rs`); the functions here
//! are call-site marshalling — push args, `Call(helper_fn_idx)`.

use wasm_encoder::Instruction;

use crate::ast::{Expr, Spanned};

use super::super::WasmGcError;
use super::emit::emit_expr;
use super::{EmitCtx, SlotTable};

/// Phase 1.2b1.5 — call-site lowering for `Console.print` /
/// `Console.error` / `Console.warn` on `--target wasip2`.
///
/// Sequence (single arg `s: String`):
///   1. Lazy-init the cached `output-stream` handle. The handle
///      global starts at `-1`; on first call, invoke
///      `wasi:cli/{stdout,stderr}.get-stdout/stderr` (returns the
///      i32 resource handle) and store it.
///   2. Push `s` (engine-GC `(ref null $string)`), call
///      `__rt_string_to_lm` — that helper writes the utf-8 bytes
///      to LM[0..len], grows memory if needed, and returns `len`.
///      Stash `len` in the per-fn i32 scratch slot.
///   3. Defensive `memory.grow(1)` so the retptr area
///      `[(len+15)&-16, (len+15)&-16 + 12)` cannot fall past the
///      memory boundary even when `len` lands on a page boundary.
///   4. Call `wasi:io/streams.[method]output-stream.blocking-write-
///      and-flush(handle, ptr=0, len, retptr=(len+15)&-16)`. The
///      host writes a 12-byte `result<_, stream-error>` tag at
///      `retptr`; we ignore it (Aver `Console.*` is `Unit`).
pub(super) fn emit_console_print_wasip2(
    func: &mut wasm_encoder::Function,
    method: &str,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation(
            "emit_console_print_wasip2 invoked without wasip2 lowering ctx".into(),
        )
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Console.{method} on `--target wasip2` expects 1 arg (the String), got {}",
            args.len()
        )));
    }
    let scratch = slots.console_print_wasip2_scratch.ok_or_else(|| {
        WasmGcError::Validation(
            "Console.* on wasip2: [len, offset] scratch slots were not allocated by \
             SlotTable — `fn_needs_console_print_wasip2_scratch` did not flag this fn"
                .into(),
        )
    })?;
    let len_local = scratch[0];
    let off_local = scratch[1];

    // Pick stream: stdout for `print`, stderr for `error` / `warn`.
    // The matching `get_*_fn_idx` and `*_handle_global` must be
    // populated whenever this method's effect is registered;
    // anything else is a wiring bug in `module::emit_module_with`.
    let (handle_global, get_fn) = match method {
        "print" => (
            lowering.stdout_handle_global.ok_or_else(|| {
                WasmGcError::Validation(
                    "Console.print on wasip2: stdout_handle global missing — \
                     wasip2_imports did not register CliGetStdout"
                        .into(),
                )
            })?,
            lowering.get_stdout_fn_idx.ok_or_else(|| {
                WasmGcError::Validation("Console.print on wasip2: get_stdout fn idx missing".into())
            })?,
        ),
        "error" | "warn" => (
            lowering.stderr_handle_global.ok_or_else(|| {
                WasmGcError::Validation(
                    "Console.error/warn on wasip2: stderr_handle global missing".into(),
                )
            })?,
            lowering.get_stderr_fn_idx.ok_or_else(|| {
                WasmGcError::Validation(
                    "Console.error/warn on wasip2: get_stderr fn idx missing".into(),
                )
            })?,
        ),
        _ => {
            return Err(WasmGcError::Validation(format!(
                "Console.{method} is not lowered on `--target wasip2`"
            )));
        }
    };

    // Step 1: lazy-init handle.
    func.instruction(&Instruction::GlobalGet(handle_global));
    func.instruction(&Instruction::I32Const(-1));
    func.instruction(&Instruction::I32Eq);
    func.instruction(&Instruction::If(wasm_encoder::BlockType::Empty));
    func.instruction(&Instruction::Call(get_fn));
    func.instruction(&Instruction::GlobalSet(handle_global));
    func.instruction(&Instruction::End);

    // Step 2: marshal s → LM[0..len], stash len.
    let str_to_lm = lowering.str_to_lm_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Console.* on wasip2: __rt_string_to_lm fn idx missing — bridge not allocated".into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?;
    func.instruction(&Instruction::Call(str_to_lm));
    func.instruction(&Instruction::LocalSet(len_local));

    // Step 3: defensive memory.grow(1) so retptr+12 stays in-bounds
    // even when len landed exactly on a page boundary.
    func.instruction(&Instruction::I32Const(1));
    func.instruction(&Instruction::MemoryGrow(0));
    func.instruction(&Instruction::Drop);

    let write_fn = lowering.blocking_write_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Console.* on wasip2: blocking-write-and-flush fn idx missing".into(),
        )
    })?;

    // Step 4: chunked write loop. The shared helper walks
    // LM[0..len] in 4096-byte slices (wasmtime-wasi caps single
    // blocking-write-and-flush calls there). retptr is computed
    // inline as `(len + 15) & -16` (16-byte aligned, just past
    // the string bytes); since `len` is a captured i32 local the
    // closure recomputes the alignment per iteration — wasmtime
    // optimises the trivial arithmetic, and we save a scratch
    // slot. Errors are ignored (Console.* is Unit, fire-and-
    // forget; matches the AverBridge / VM target semantics).
    super::super::wasip2_helpers::emit_chunked_blocking_write(
        func,
        len_local,
        off_local,
        write_fn,
        &|f| {
            f.instruction(&Instruction::GlobalGet(handle_global));
        },
        &|f| {
            f.instruction(&Instruction::LocalGet(len_local));
            f.instruction(&Instruction::I32Const(15));
            f.instruction(&Instruction::I32Add);
            f.instruction(&Instruction::I32Const(-16));
            f.instruction(&Instruction::I32And);
        },
        None,
    );
    Ok(())
}

/// Phase 1.3.2 — `Args.get() -> List<String>` on `--target wasip2`.
///
/// Allocates an 8-byte retptr area via `cabi_realloc(0, 0, 4, 8)`,
/// calls `wasi:cli/environment.get-arguments(retptr)` (the host
/// uses `cabi_realloc` again to allocate the list payload bytes
/// in guest memory), then hands the retptr to the shared
/// `__rt_canonical_decode_list_string` helper which walks
/// `(list_ptr, list_len)` + per-entry `(str_ptr, str_len)` into a
/// cons-built Aver `List<String>`. Five instructions at the call
/// site; the per-element copy lives in the helper.
pub(super) fn emit_args_get_wasip2(
    func: &mut wasm_encoder::Function,
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Args.get on wasip2: lowering ctx missing".into())
    })?;
    let cabi_realloc = lowering.cabi_realloc_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Args.get on wasip2: cabi_realloc fn idx missing — wasip2_imports must register \
             at least one slot for cabi_realloc to be allocated"
                .into(),
        )
    })?;
    let get_arguments = lowering.get_arguments_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Args.get on wasip2: wasi:cli/environment.get-arguments fn idx missing".into(),
        )
    })?;
    let decoder = lowering.decode_list_string_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Args.get on wasip2: __rt_canonical_decode_list_string fn idx missing".into(),
        )
    })?;
    let retptr_local = slots.args_get_wasip2_retptr_scratch.ok_or_else(|| {
        WasmGcError::Validation(
            "Args.get on wasip2: i32 retptr scratch slot missing — \
             SlotTable should have allocated via fn_needs_args_get_scratch"
                .into(),
        )
    })?;

    // retptr = cabi_realloc(0, 0, 4, 8)  (8 bytes for the list_ptr/
    // list_len pair, 4-byte aligned).
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I32Const(4));
    func.instruction(&Instruction::I32Const(8));
    func.instruction(&Instruction::Call(cabi_realloc));
    func.instruction(&Instruction::LocalSet(retptr_local));

    // Host call writes (list_ptr, list_len) + per-entry (str_ptr,
    // str_len) + utf-8 bytes via further cabi_realloc calls.
    func.instruction(&Instruction::LocalGet(retptr_local));
    func.instruction(&Instruction::Call(get_arguments));

    // Decoder pushes the materialised List<String> onto the stack.
    func.instruction(&Instruction::LocalGet(retptr_local));
    func.instruction(&Instruction::Call(decoder));
    Ok(())
}

/// Phase 1.3.3 — `Env.get(name: String) -> Option<String>` on
/// `--target wasip2`. Marshals the key via `__rt_string_to_lm`
/// (writes utf-8 bytes at LM[0..key_len], returns key_len),
/// allocates a fresh 8-byte retptr via `cabi_realloc` (lands at
/// >= page 2, disjoint from key bytes), calls
/// > `wasi:cli/environment.get-environment(retptr)` (the host fills
/// > the retptr area with `(list_ptr, list_len)` and uses
/// > `cabi_realloc` callbacks for per-entry `(key_ptr, key_len,
/// val_ptr, val_len)` blocks plus the utf-8 buffers), then hands
/// > `(retptr, key_ptr=0, key_len)` to `__rt_canonical_env_lookup`
/// > which linear-searches and returns `Option.Some(value)` on hit
/// > or `Option.None` on miss — the helper wraps the discriminant
/// > itself so call sites don't need to.
pub(super) fn emit_env_get_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx
        .wasip2_lowering
        .ok_or_else(|| WasmGcError::Validation("Env.get on wasip2: lowering ctx missing".into()))?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Env.get on `--target wasip2` expects 1 arg (the key String), got {}",
            args.len()
        )));
    }
    let str_to_lm = lowering.str_to_lm_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Env.get on wasip2: __rt_string_to_lm fn idx missing — bridge not allocated".into(),
        )
    })?;
    let cabi_realloc = lowering.cabi_realloc_fn_idx.ok_or_else(|| {
        WasmGcError::Validation("Env.get on wasip2: cabi_realloc fn idx missing".into())
    })?;
    let get_environment = lowering.get_environment_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Env.get on wasip2: wasi:cli/environment.get-environment fn idx missing".into(),
        )
    })?;
    let lookup = lowering.env_get_lookup_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Env.get on wasip2: __rt_canonical_env_lookup fn idx missing".into(),
        )
    })?;
    let scratch = slots.env_get_wasip2_scratch.ok_or_else(|| {
        WasmGcError::Validation(
            "Env.get on wasip2: [retptr, key_len] scratch pair missing — \
             SlotTable should have allocated via fn_needs_env_get_wasip2_scratch"
                .into(),
        )
    })?;
    let retptr_local = scratch[0];
    let key_len_local = scratch[1];

    // key bytes → LM[0..key_len], stash key_len.
    emit_expr(func, &args[0], slots, ctx)?;
    func.instruction(&Instruction::Call(str_to_lm));
    func.instruction(&Instruction::LocalSet(key_len_local));

    // retptr = cabi_realloc(0, 0, 4, 8).
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I32Const(4));
    func.instruction(&Instruction::I32Const(8));
    func.instruction(&Instruction::Call(cabi_realloc));
    func.instruction(&Instruction::LocalSet(retptr_local));

    // Host call writes (list_ptr, list_len) at retptr + per-entry
    // tuples + utf-8 buffers (all via further cabi_realloc bumps).
    func.instruction(&Instruction::LocalGet(retptr_local));
    func.instruction(&Instruction::Call(get_environment));

    // __rt_canonical_env_lookup(retptr, key_ptr=0, key_len) →
    // matching value String (or empty on no-match).
    func.instruction(&Instruction::LocalGet(retptr_local));
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::LocalGet(key_len_local));
    func.instruction(&Instruction::Call(lookup));
    Ok(())
}

/// Phase 1.4 — `Time.unixMs() -> Int` on `--target wasip2`.
///
/// Lowers to `wasi:clocks/wall-clock.now: () -> datetime` (canonical
/// ABI: `(retptr: i32) -> ()`; host writes 16 bytes at retptr —
/// `seconds: u64` at +0, `nanoseconds: u32` at +8, 4 bytes pad).
/// Reads back the two fields, computes
/// `seconds * 1000 + nanoseconds / 1_000_000` as i64.
///
/// Retptr placement: `LM[0..16]`. Console.print's transport buffer
/// also writes at LM[0..len], but the two effects run sequentially
/// inside the guest — Console.print bytes are stale from the host's
/// perspective the moment that call returns, so reusing LM[0..16]
/// for the clocks retptr is sound. Memory section is unconditionally
/// emitted on wasip2 with imports, so the page-1 LM is available.
pub(super) fn emit_time_unix_ms_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Time.unixMs on wasip2: lowering ctx missing".into())
    })?;
    if !args.is_empty() {
        return Err(WasmGcError::Validation(format!(
            "Time.unixMs on `--target wasip2` expects 0 args, got {}",
            args.len()
        )));
    }
    let now_fn = lowering.clocks_now_fn_idx.ok_or_else(|| {
        WasmGcError::Validation("Time.unixMs on wasip2: clocks-now fn idx missing".into())
    })?;
    // Call now(retptr=0). Host writes datetime to LM[0..16].
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::Call(now_fn));
    // unixMs = seconds * 1000 + (nanoseconds / 1_000_000)
    //        = i64.load LM[0]  * 1000
    //        + i64.extend_i32_u (i32.load LM[8]) / 1_000_000
    let mem_arg = wasm_encoder::MemArg {
        offset: 0,
        align: 3, // log2(8) — i64 alignment
        memory_index: 0,
    };
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I64Load(mem_arg));
    func.instruction(&Instruction::I64Const(1000));
    func.instruction(&Instruction::I64Mul);
    let ns_mem_arg = wasm_encoder::MemArg {
        offset: 8,
        align: 2, // log2(4) — i32 alignment
        memory_index: 0,
    };
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I32Load(ns_mem_arg));
    func.instruction(&Instruction::I64ExtendI32U);
    func.instruction(&Instruction::I64Const(1_000_000));
    func.instruction(&Instruction::I64DivU);
    func.instruction(&Instruction::I64Add);
    Ok(())
}

/// Phase 1.4b — `Time.now() -> String` on `--target wasip2`.
///
/// Calls `wasi:clocks/wall-clock.now(retptr=0)` (the host writes
/// `(seconds: u64, nanoseconds: u32)` into LM[0..16] — same retptr
/// shape Time.unixMs already uses), loads the two fields, and
/// hands them to `__rt_format_iso8601` which materialises a fresh
/// 24-byte `(array i8)` containing the RFC3339-like string. The
/// helper itself never reads LM, so the LM[0..16] window is free
/// to be reused immediately afterwards.
pub(super) fn emit_time_now_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Time.now on wasip2: lowering ctx missing".into())
    })?;
    if !args.is_empty() {
        return Err(WasmGcError::Validation(format!(
            "Time.now on `--target wasip2` expects 0 args, got {}",
            args.len()
        )));
    }
    let now_fn = lowering.clocks_now_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Time.now on wasip2: clocks-now fn idx missing — slot not registered".into(),
        )
    })?;
    let fmt_fn = lowering.fmt_iso8601_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Time.now on wasip2: __rt_format_iso8601 fn idx missing — helper not allocated".into(),
        )
    })?;

    // now(retptr=0). Host writes 16 bytes at LM[0..16].
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::Call(now_fn));

    // Load secs (i64 @ LM[0]) and nanos (i32 @ LM[8]); push to
    // stack in `(secs, nanos)` order matching the helper's params.
    let secs_mem = wasm_encoder::MemArg {
        offset: 0,
        align: 3, // log2(8)
        memory_index: 0,
    };
    let nanos_mem = wasm_encoder::MemArg {
        offset: 8,
        align: 2, // log2(4)
        memory_index: 0,
    };
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I64Load(secs_mem));
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I32Load(nanos_mem));

    // Helper returns the formatted ref string on the stack.
    func.instruction(&Instruction::Call(fmt_fn));
    Ok(())
}

/// Phase 1.3.4 — `Console.readLine() -> Result<String, String>` on
/// `--target wasip2`. The whole machinery — stdin handle caching,
/// 1-byte blocking-read loop, buffer growth, `\n` / `\r` handling,
/// `Result` construction — lives in the
/// `__rt_console_read_line` helper. The call site is one
/// instruction: `Call $__rt_console_read_line`.
pub(super) fn emit_console_read_line_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Console.readLine on wasip2: lowering ctx missing".into())
    })?;
    if !args.is_empty() {
        return Err(WasmGcError::Validation(format!(
            "Console.readLine on `--target wasip2` expects 0 args, got {}",
            args.len()
        )));
    }
    let read_fn = lowering.console_read_line_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Console.readLine on wasip2: __rt_console_read_line fn idx missing — \
             helper not allocated"
                .into(),
        )
    })?;
    func.instruction(&Instruction::Call(read_fn));
    Ok(())
}

/// Phase 1.4c — `Time.sleep(ms: Int) -> Unit` on `--target wasip2`.
///
/// Emits the milliseconds expression onto the stack and calls
/// `__rt_time_sleep`, which subscribes a pollable on the
/// monotonic clock, waits for it via `wasi:io/poll.poll`, and
/// drops the pollable. Source-level Aver still sees the same
/// `Time.sleep(ms)` it sees on the VM target — pollables are
/// implementation detail.
pub(super) fn emit_time_sleep_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Time.sleep on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Time.sleep on `--target wasip2` expects 1 arg (ms: Int), got {}",
            args.len()
        )));
    }
    let sleep_fn = lowering.time_sleep_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Time.sleep on wasip2: __rt_time_sleep fn idx missing — helper not allocated".into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?; // ms: i64
    func.instruction(&Instruction::Call(sleep_fn));
    Ok(())
}

/// Phase 1.5.1 — `Disk.exists(path: String) -> Bool` on
/// `--target wasip2`. Emits the path expression onto the stack
/// and calls `__rt_disk_exists`, which lazy-fetches a preopen
/// descriptor, runs `stat-at`, and returns the boolean tag.
/// `false` on no-preopens / Err / wasi-error; `true` on Ok.
pub(super) fn emit_disk_exists_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.exists on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Disk.exists on `--target wasip2` expects 1 arg (path: String), got {}",
            args.len()
        )));
    }
    let exists_fn = lowering.disk_exists_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Disk.exists on wasip2: __rt_disk_exists fn idx missing — \
             helper not allocated"
                .into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?; // path: ref string
    func.instruction(&Instruction::Call(exists_fn));
    Ok(())
}

/// Phase 1.5.2 — `Disk.readText(path: String) ->
/// Result<String, String>` on `--target wasip2`. Pushes the path
/// expression onto the stack and calls `__rt_disk_read_text`,
/// which owns the open-at + read-via-stream + blocking-read loop
/// + per-call resource drops.
pub(super) fn emit_disk_read_text_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.readText on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Disk.readText on `--target wasip2` expects 1 arg (path: String), got {}",
            args.len()
        )));
    }
    let read_fn = lowering.disk_read_text_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Disk.readText on wasip2: __rt_disk_read_text fn idx missing — \
             helper not allocated"
                .into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?; // path: ref string
    func.instruction(&Instruction::Call(read_fn));
    Ok(())
}

/// Phase 1.4 — `Random.int(min: Int, max: Int) -> Int` on
/// `--target wasip2`.
///
/// Lowers to `wasi:random/random.get-random-u64: () -> u64` plus
/// guest-side modulo by `(max - min + 1)` and offset by `min`. The
/// modulo is the standard slightly-biased pattern (acceptable for
/// non-cryptographic use, matches the wasm-gc target's existing
/// shape via `aver/random_int`).
pub(super) fn emit_random_int_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Random.int on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 2 {
        return Err(WasmGcError::Validation(format!(
            "Random.int on `--target wasip2` expects 2 args (min, max), got {}",
            args.len()
        )));
    }
    let rand_fn = lowering.random_u64_fn_idx.ok_or_else(|| {
        WasmGcError::Validation("Random.int on wasip2: random get-random-u64 fn idx missing".into())
    })?;
    let min_scratch = slots.random_int_wasip2_min_scratch.ok_or_else(|| {
        WasmGcError::Validation(
            "Random.int on wasip2: i64 min scratch slot missing — \
             SlotTable should have allocated via fn_needs_random_int_wasip2_scratch"
                .into(),
        )
    })?;
    // result = min + ((u64 % (max - min + 1)) as i64).
    //
    // `min` is referenced twice (the offset and `max - min`), so we
    // evaluate `args[0]` exactly once into a scratch local. Without
    // this, `Random.int(readBound(), 10)` would call `readBound()`
    // twice — call-by-value violation, and any host-visible side
    // effect inside `readBound` would fire twice.
    //
    // Stack discipline:
    //   eval args[0] → LocalSet(min_scratch)
    //   push min_scratch
    //   push (get-random-u64() % (max - min + 1))
    //   i64.add
    emit_expr(func, &args[0], slots, ctx)?; // min: i64
    func.instruction(&Instruction::LocalSet(min_scratch));
    func.instruction(&Instruction::LocalGet(min_scratch));
    func.instruction(&Instruction::Call(rand_fn)); // u64 -> i64 representation
    emit_expr(func, &args[1], slots, ctx)?; // max
    func.instruction(&Instruction::LocalGet(min_scratch));
    func.instruction(&Instruction::I64Sub);
    func.instruction(&Instruction::I64Const(1));
    func.instruction(&Instruction::I64Add);
    func.instruction(&Instruction::I64RemU); // modulo unsigned
    func.instruction(&Instruction::I64Add);
    Ok(())
}

/// Phase 1.4 — `Random.float() -> Float` on `--target wasip2`.
///
/// Lowers to `wasi:random/random.get-random-u64: () -> u64` plus
/// the standard 53-bit-precision scale to `[0.0, 1.0)`:
///   `(u64 >> 11) * 2^-53`.
/// Matches the convention used by JS `Math.random` and Rust's
/// `rand::Rng::gen::<f64>()` — both produce 53 random mantissa
/// bits with no exponent bits set.
pub(super) fn emit_random_float_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Random.float on wasip2: lowering ctx missing".into())
    })?;
    if !args.is_empty() {
        return Err(WasmGcError::Validation(format!(
            "Random.float on `--target wasip2` expects 0 args, got {}",
            args.len()
        )));
    }
    let rand_fn = lowering.random_u64_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Random.float on wasip2: random get-random-u64 fn idx missing".into(),
        )
    })?;
    func.instruction(&Instruction::Call(rand_fn));
    func.instruction(&Instruction::I64Const(11));
    func.instruction(&Instruction::I64ShrU);
    func.instruction(&Instruction::F64ConvertI64U);
    // 2^-53 = 1.0 / (1 << 53). Computed as a literal const.
    func.instruction(&Instruction::F64Const(
        (1.0_f64 / (1u64 << 53) as f64).into(),
    ));
    func.instruction(&Instruction::F64Mul);
    Ok(())
}

/// Phase 1.5.3 — `Disk.writeText(path: String, content: String)
/// -> Result<Unit, String>` on `--target wasip2`. Pushes both
/// args onto the stack and calls `__rt_disk_write_text`, which
/// owns the open-at(create+truncate) + write-via-stream +
/// blocking-write-and-flush + per-call resource drops.
pub(super) fn emit_disk_write_text_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.writeText on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 2 {
        return Err(WasmGcError::Validation(format!(
            "Disk.writeText on `--target wasip2` expects 2 args (path, content), got {}",
            args.len()
        )));
    }
    let write_fn = lowering.disk_write_text_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Disk.writeText on wasip2: __rt_disk_write_text fn idx missing — \
             helper not allocated"
                .into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?; // path
    emit_expr(func, &args[1], slots, ctx)?; // content
    func.instruction(&Instruction::Call(write_fn));
    Ok(())
}

/// Phase 1.5.4 — `Disk.delete(path) -> Result<Unit, String>` on
/// `--target wasip2`. Pushes the path onto the stack and calls
/// `__rt_disk_delete` (single wasi `unlink-file-at` underneath).
pub(super) fn emit_disk_delete_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.delete on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Disk.delete on `--target wasip2` expects 1 arg (path), got {}",
            args.len()
        )));
    }
    let fn_idx = lowering.disk_delete_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Disk.delete on wasip2: __rt_disk_delete fn idx missing — helper not allocated".into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?;
    func.instruction(&Instruction::Call(fn_idx));
    Ok(())
}

/// Phase 1.5.4 — `Disk.deleteDir(path) -> Result<Unit, String>`.
pub(super) fn emit_disk_delete_dir_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.deleteDir on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Disk.deleteDir on `--target wasip2` expects 1 arg (path), got {}",
            args.len()
        )));
    }
    let fn_idx = lowering.disk_delete_dir_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Disk.deleteDir on wasip2: __rt_disk_delete_dir fn idx missing".into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?;
    func.instruction(&Instruction::Call(fn_idx));
    Ok(())
}

/// Phase 1.5.4 — `Disk.makeDir(path) -> Result<Unit, String>`.
pub(super) fn emit_disk_make_dir_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.makeDir on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Disk.makeDir on `--target wasip2` expects 1 arg (path), got {}",
            args.len()
        )));
    }
    let fn_idx = lowering.disk_make_dir_fn_idx.ok_or_else(|| {
        WasmGcError::Validation("Disk.makeDir on wasip2: __rt_disk_make_dir fn idx missing".into())
    })?;
    emit_expr(func, &args[0], slots, ctx)?;
    func.instruction(&Instruction::Call(fn_idx));
    Ok(())
}

/// Phase 1.5.5 — `Disk.appendText(path, content) ->
/// Result<Unit, String>` on `--target wasip2`. Pushes both args
/// and calls `__rt_disk_append_text`, which uses the same body
/// emitter as `__rt_disk_write_text` flipped to append mode.
pub(super) fn emit_disk_append_text_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.appendText on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 2 {
        return Err(WasmGcError::Validation(format!(
            "Disk.appendText on `--target wasip2` expects 2 args (path, content), got {}",
            args.len()
        )));
    }
    let fn_idx = lowering.disk_append_text_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(
            "Disk.appendText on wasip2: __rt_disk_append_text fn idx missing".into(),
        )
    })?;
    emit_expr(func, &args[0], slots, ctx)?;
    emit_expr(func, &args[1], slots, ctx)?;
    func.instruction(&Instruction::Call(fn_idx));
    Ok(())
}

/// Phase 1.5.6 — `Disk.listDir(path) -> Result<List<String>, String>`
/// on `--target wasip2`. Pushes the path arg and calls
/// `__rt_disk_list_dir`, which owns the open-at(directory) +
/// read-directory + entry-iteration loop + drops.
pub(super) fn emit_disk_list_dir_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation("Disk.listDir on wasip2: lowering ctx missing".into())
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "Disk.listDir on `--target wasip2` expects 1 arg (path), got {}",
            args.len()
        )));
    }
    let fn_idx = lowering.disk_list_dir_fn_idx.ok_or_else(|| {
        WasmGcError::Validation("Disk.listDir on wasip2: __rt_disk_list_dir fn idx missing".into())
    })?;
    emit_expr(func, &args[0], slots, ctx)?;
    func.instruction(&Instruction::Call(fn_idx));
    Ok(())
}

/// Phase 2 — `Http.*(url[, content_type, body, headers]) ->
/// Result<HttpResponse, String>` on `--target wasip2`. The shared
/// `__rt_http_request` helper takes 5 params: method_tag i32,
/// url ref string, content_type ref string, body ref string,
/// headers ref map. Per-method dispatchers push the appropriate
/// method ordinal from wasi:http's `method` variant (0=GET,
/// 1=HEAD, 2=POST, 3=PUT, 4=DELETE, 8=PATCH).
///
/// For body-less methods (GET/HEAD/DELETE) the dispatcher
/// synthesises empty content_type / body / headers — the helper
/// gates body marshalling on `method >= 2 && method != 4` and
/// the headers-iter loop is a cap-bounded no-op on an empty map.
fn emit_http_simple_method_wasip2(
    method_name: &str,
    method_tag: i32,
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation(format!("{method_name} on wasip2: lowering ctx missing"))
    })?;
    if args.len() != 1 {
        return Err(WasmGcError::Validation(format!(
            "{method_name} on `--target wasip2` expects 1 arg (url), got {}",
            args.len()
        )));
    }
    let fn_idx = lowering.http_get_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(format!(
            "{method_name} on wasip2: __rt_http_request fn idx missing"
        ))
    })?;
    let registry = ctx.registry;
    let string_idx = registry.string_array_type_idx.ok_or_else(|| {
        WasmGcError::Validation(format!("{method_name} on wasip2: string type idx missing"))
    })?;
    let map_slots = registry
        .map_slots("Map<String,List<String>>")
        .ok_or_else(|| {
            WasmGcError::Validation(format!(
                "{method_name} on wasip2: Map<String, List<String>> slots missing"
            ))
        })?;

    func.instruction(&Instruction::I32Const(method_tag));
    emit_expr(func, &args[0], slots, ctx)?;
    // Empty content_type
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::ArrayNewDefault(string_idx));
    // Empty body
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::ArrayNewDefault(string_idx));
    // Empty headers map (size=0, cap=INITIAL_CAP, default arrays).
    // INITIAL_CAP must match emit_map_empty in maps.rs (16384).
    const INITIAL_CAP: i32 = 16384;
    func.instruction(&Instruction::I32Const(0));
    func.instruction(&Instruction::I32Const(INITIAL_CAP));
    func.instruction(&Instruction::I32Const(INITIAL_CAP));
    func.instruction(&Instruction::ArrayNewDefault(map_slots.keys_array));
    func.instruction(&Instruction::I32Const(INITIAL_CAP));
    func.instruction(&Instruction::ArrayNewDefault(map_slots.values_array));
    func.instruction(&Instruction::StructNew(map_slots.map));
    func.instruction(&Instruction::Call(fn_idx));
    Ok(())
}

/// Body-bearing dispatch shared by POST/PUT/PATCH. Aver source
/// signature: `(url: String, content_type: String, body: String,
/// headers: Map<String, List<String>>) -> Result<HttpResponse,
/// String>`.
fn emit_http_body_method_wasip2(
    method_name: &str,
    method_tag: i32,
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    let lowering = ctx.wasip2_lowering.ok_or_else(|| {
        WasmGcError::Validation(format!("{method_name} on wasip2: lowering ctx missing"))
    })?;
    if args.len() != 4 {
        return Err(WasmGcError::Validation(format!(
            "{method_name} on `--target wasip2` expects 4 args (url, content_type, body, headers), got {}",
            args.len()
        )));
    }
    let fn_idx = lowering.http_get_fn_idx.ok_or_else(|| {
        WasmGcError::Validation(format!(
            "{method_name} on wasip2: __rt_http_request fn idx missing"
        ))
    })?;
    func.instruction(&Instruction::I32Const(method_tag));
    emit_expr(func, &args[0], slots, ctx)?; // url
    emit_expr(func, &args[1], slots, ctx)?; // content_type
    emit_expr(func, &args[2], slots, ctx)?; // body
    emit_expr(func, &args[3], slots, ctx)?; // headers
    func.instruction(&Instruction::Call(fn_idx));
    Ok(())
}

pub(super) fn emit_http_get_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    emit_http_simple_method_wasip2("Http.get", 0, func, args, slots, ctx)
}

pub(super) fn emit_http_head_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    emit_http_simple_method_wasip2("Http.head", 1, func, args, slots, ctx)
}

pub(super) fn emit_http_delete_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    emit_http_simple_method_wasip2("Http.delete", 4, func, args, slots, ctx)
}

pub(super) fn emit_http_post_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    emit_http_body_method_wasip2("Http.post", 2, func, args, slots, ctx)
}

pub(super) fn emit_http_put_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    emit_http_body_method_wasip2("Http.put", 3, func, args, slots, ctx)
}

pub(super) fn emit_http_patch_wasip2(
    func: &mut wasm_encoder::Function,
    args: &[Spanned<Expr>],
    slots: &SlotTable,
    ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
    emit_http_body_method_wasip2("Http.patch", 8, func, args, slots, ctx)
}