aver-lang 0.22.1

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
//! `__rt_tcp_connect` — the big one. DNS resolve → create-socket
//! → start/finish-connect → pool slot allocate → Tcp.Connection
//! materialise pipeline. See the per-step comments inline for the
//! phase 4.2.2a–4.2.2f breakdown.

use wasm_encoder::{Function, Instruction, ValType};

use super::restore_bump;

/// Slot bundle for `__rt_tcp_connect`. Mirrors `HttpGetIndices`'s
/// shape — type idxs + helper fn idxs the body needs to resolve.
/// Phase 4.2.2a adds the network-handle global so the prolog can
/// lazy-init it; the stub body otherwise still ignores host / port.
pub(in crate::codegen::wasm_gc) struct TcpConnectIndices {
    /// `(param (ref null $string)) (param i64) (result (ref null
    /// $result_tcp_connection_string))` function type idx.
    pub fn_type: u32,
    /// Allocated wasm fn idx — call-site emitters reference this
    /// when lowering `Tcp.connect`.
    pub fn_idx: u32,
    /// Wasm-gc type idx for `(array i8)` String repr — needed by
    /// the stub body to materialise the placeholder error message.
    pub string_type_idx: u32,
    /// Passive data segment idx carrying the bytes
    /// `"tcp: connect not yet implemented"`. The stub builds an Aver
    /// String from this via `array.new_data`. Replaced with the
    /// real per-failure-mode segments in Phase 4.2.2+.
    pub stub_err_segment_idx: u32,
    /// Length of the placeholder error bytes.
    pub stub_err_len: u32,
    /// Phase 4.2.2b — passive data segment + length for the bytes
    /// `"tcp: dns resolve failed"`. Returned via Result.Err when
    /// `resolve-addresses` itself fails (host syntactically invalid,
    /// resolver permanent failure). Per-error-code dispatch lands
    /// in a follow-up — the v1 generic message keeps the body small.
    pub dns_err_segment_idx: u32,
    pub dns_err_len: u32,
    /// Phase 4.2.2b2 — `"tcp: dns no addresses"`. Returned when the
    /// resolver finished without yielding any IPv4 address: either
    /// `resolve-next-address` Err mid-stream, or the stream
    /// exhausted (`None`) before an IPv4 surfaced. IPv6-only hosts
    /// fall into this bucket today — IPv6 support waits on the
    /// full ip-socket-address lowering (out of 0.20 scope).
    pub no_addr_segment_idx: u32,
    pub no_addr_len: u32,
    /// Phase 4.2.2c — `"tcp: socket create failed"`. `create-tcp-
    /// socket(ipv4)` Err (system limit, AF unsupported on host).
    pub sock_err_segment_idx: u32,
    pub sock_err_len: u32,
    /// Phase 4.2.2c — `"tcp: connect failed"`. Covers both
    /// `start-connect` Err (invalid address) and `finish-connect`
    /// Err (connection-refused, timeout, host-unreachable, etc.).
    /// Per-error-code dispatch is a follow-up — generic message
    /// keeps the body small.
    pub conn_err_segment_idx: u32,
    pub conn_err_len: u32,
    /// Phase 4.7+ fix #7 — `"tcp: port out of range"`. Surfaces
    /// when the caller hands a port outside `0..=65535`. VM ships
    /// `Tcp: port {N} is out of range (0\u{2013}65535)`; we use a
    /// canned static string because the wasip2 Err is materialised
    /// from a data segment, not runtime formatting.
    pub port_err_segment_idx: u32,
    pub port_err_len: u32,
    /// Phase 4.7+ fix #10 — `"tcp: connection limit reached (256
    /// max)"`. Pool wraparound used to silently evict the live
    /// occupant of slot `tcp_next_id & 255`; `aver-rt::tcp::connect`
    /// refuses with this exact message. Refuses to fresh-connect
    /// before any socket / connect resources are claimed.
    pub limit_err_segment_idx: u32,
    pub limit_err_len: u32,
}

/// Pool storage + bump-heap rewind. Grouped because every
/// successful connect touches all five: it writes a new slot into
/// `tcp_pool` (typed `$tcp_pool` / element type `$tcp_slot`),
/// bumps `tcp_next_id`, and rewinds `bump_alloc_ptr` on each exit
/// path to reclaim the per-call scratch.
pub(in crate::codegen::wasm_gc) struct TcpConnectPool {
    /// `tcp_next_id: i32 = 0` mutable global. Bumps after each
    /// successful connect; slot_idx is `tcp_next_id & 255` (pool
    /// capacity = 256).
    pub tcp_next_id_global: u32,
    /// `tcp_pool: ref null $tcp_pool` mutable global. Lazy-
    /// allocated to `array.new_default $tcp_pool 256` on the first
    /// successful connect; subsequent connects
    /// `array.set $tcp_pool [slot_idx] := struct.new $tcp_slot
    /// (socket, in_stream, out_stream, 1)`.
    pub tcp_pool_global: u32,
    /// `$tcp_slot` wasm-gc type idx. Used by the `struct.new`
    /// building each pool entry.
    pub tcp_slot_type_idx: u32,
    /// `$tcp_pool` wasm-gc type idx. Used by the
    /// `array.new_default` lazy-alloc + `array.set` slot writes.
    pub tcp_pool_type_idx: u32,
    /// Phase 4.2.2f — `bump_alloc_ptr` global. Saved on entry,
    /// restored on every exit so the helper's `cabi_realloc` calls
    /// reclaim their scratch as soon as the call returns. Without
    /// this Tcp.connect leaks ~64 bytes of bump heap per call.
    pub bump_alloc_ptr_global: u32,
}

/// DNS pipeline — instance-network lazy fetch, resolve-addresses
/// retptr setup, and the resolve-next-address polling loop.
pub(in crate::codegen::wasm_gc) struct TcpConnectDns {
    /// `wasi:sockets/instance-network.instance-network: func() ->
    /// network`. Phase 4.2.2a calls this once per program; the
    /// resulting resource handle lands in `network_handle_global`.
    pub instance_network_fn: u32,
    /// `network_handle: i32 = -1` mutable global. -1 sentinel ⇒
    /// "not yet fetched"; the prolog runs `instance-network()` and
    /// stores the resulting resource handle here for program
    /// lifetime. Phase 4.2.2b onwards reads it back to thread into
    /// `resolve-addresses` / `start-connect`.
    pub network_handle_global: u32,
    /// `wasi:sockets/ip-name-lookup.resolve-addresses: func(network:
    /// borrow<network>, name: string) -> result<
    ///   resolve-address-stream, error-code>`. Canonical-ABI
    /// signature: `(network, name_ptr, name_len, retptr) -> ()`;
    /// retptr is 8 bytes (tag@0, stream_handle/err_code@4).
    pub resolve_addresses_fn: u32,
    /// `[method]resolve-address-stream.resolve-next-address:
    /// (this, retptr) -> ()`. Retptr layout:
    ///   +0: result tag (0=Ok, 1=Err)
    ///   +2: on Ok→option tag (0=None, 1=Some); on Err→error-code u8
    ///   +4: on Ok+Some→ip-address variant tag (0=ipv4, 1=ipv6)
    ///   +6..+10 (ipv4): 4× u8 octets
    ///   +6..+22 (ipv6): 8× u16 hextets
    pub resolve_next_address_fn: u32,
    /// `[resource-drop]resolve-address-stream`. Phase 4.2.2b2 keeps
    /// the stream live while looping `resolve-next-address` and
    /// drops once the first IPv4 has been pulled.
    pub drop_resolve_stream_fn: u32,
    /// `[method]resolve-address-stream.subscribe: (this) ->
    /// pollable`. Returns a fresh pollable each loop iteration
    /// that becomes ready when the next address (or EOL) is
    /// available.
    pub stream_subscribe_fn: u32,
    /// `wasi:io/poll.poll: (in_ptr, in_len, retptr) -> ()`.
    /// Single-entry pollable input lives at scratch[+56], the
    /// indices-of-ready retptr at scratch[+48]. Ignores the
    /// returned indices — single-pollable poll already implies
    /// "the only one is ready".
    pub poll_fn: u32,
    /// `[resource-drop]pollable`. Each `stream.subscribe` allocates
    /// a single-use pollable that gets dropped right after `poll`
    /// returns.
    pub drop_pollable_fn: u32,
}

/// Socket-side TCP imports — create / start / finish / drop /
/// shutdown plus the stream-drop pair used for stale-slot recovery
/// when the pool wraps past 256.
pub(in crate::codegen::wasm_gc) struct TcpConnectSocket {
    /// `wasi:sockets/tcp-create-socket.create-tcp-socket: func(
    /// family) -> result<tcp-socket, error-code>`. Family is the
    /// `ip-address-family` enum (0=ipv4, 1=ipv6); we always pass 0.
    /// Result via retptr (8 bytes: tag@0 + handle/err@4).
    pub create_tcp_socket_fn: u32,
    /// `[method]tcp-socket.start-connect: (this, network,
    /// remote-address) -> result<_, error-code>`. 14 i32 args +
    /// retptr. `remote-address` is the `ip-socket-address` variant
    /// flat-lowered to 12 i32 positions (variant tag + max(ipv4 5,
    /// ipv6 11) = 12); for ipv4 the trailing 6 positions are zero.
    pub start_connect_fn: u32,
    /// `[method]tcp-socket.finish-connect: (this, retptr) -> ()`.
    /// Retptr 12 bytes: tag@0 + (in_stream@4, out_stream@8) on
    /// Ok / error-code@4 on Err.
    pub finish_connect_fn: u32,
    /// `[method]tcp-socket.subscribe: (this) -> pollable`. Same
    /// pattern as resolve-address-stream's subscribe — block until
    /// `finish-connect` is ready.
    pub socket_subscribe_fn: u32,
    /// `[resource-drop]tcp-socket`. Phase 4.2.2c drops the socket
    /// on every error path. Phase 4.2.2d keeps it live on the
    /// happy path and stashes it in the pool slot.
    pub drop_tcp_socket_fn: u32,
}

/// String marshaling + record / result wrapping — the "build the
/// user-visible heap value" side of the helper. cabi_realloc lives
/// here because every materialisation path (retptr scratch, host
/// name buffer) goes through it.
pub(in crate::codegen::wasm_gc) struct TcpConnectMaterialize {
    /// `cabi_realloc(old, old_sz, align, new_sz)` exported
    /// allocator. Used to grab the shared 64-byte scratch + the
    /// host-name LM buffer.
    pub cabi_realloc_fn: u32,
    /// `__rt_string_to_lm(s: ref string) -> i32` (length).
    /// Marshals the Aver String's bytes into LM[0..len];
    /// `resolve-addresses` reads the host name from that range.
    pub str_to_lm_fn: u32,
    /// `__rt_tcp_format_id(slot_idx: i32) -> ref string`.
    /// Materialises `"tcp-{slot_idx}"` as a fresh Aver
    /// `(array i8)`.
    pub format_id_fn: u32,
    /// `__rt_record_tcp_connection_make(id, host, port) ->
    /// ref Tcp.Connection`. Factory generated by the existing
    /// `factory_exports` machinery.
    pub record_make_fn: u32,
    /// `__rt_result_tcp_connection_string_ok(conn) -> ref Result<
    /// Tcp.Connection, String>`. Wraps the freshly-built
    /// `Tcp.Connection` on the happy path.
    pub result_ok_fn: u32,
    /// `__rt_result_tcp_connection_string_err(message: ref string)
    /// -> ref Result<Tcp.Connection, String>`. Used at every Err
    /// return point.
    pub result_err_fn: u32,
}

/// Helper fn idxs + global idxs the connect body calls, grouped by
/// pipeline concern. The body code does `helpers.pool.tcp_pool_global`
/// etc., which keeps the 27-field bundle navigable when you're
/// reading the lowering inline. Construction site in
/// `module.rs::emit_tcp_helper_bodies` builds the four sub-bundles
/// from the surrounding wireup locals.
pub(in crate::codegen::wasm_gc) struct TcpConnectHelperFns {
    pub pool: TcpConnectPool,
    pub dns: TcpConnectDns,
    pub socket: TcpConnectSocket,
    pub materialize: TcpConnectMaterialize,
}

/// Emit the body for `__rt_tcp_connect`. Phase 4.2.2a layout:
///
/// 1. Lazy-init `network_handle` from
///    `wasi:sockets/instance-network.instance-network` on first call.
/// 2. STUB tail (Phase 4.2.1): ignore the (host, port) params, push
///    the placeholder error string, wrap via the Result.Err factory,
///    return.
///
/// The host / port args reach the locals (`local 0`, `local 1`) but
/// the body never reads them yet — that lands in 4.2.2b when the DNS
/// resolve loop replaces the stub tail.
/// Single shared retptr / pollable-input scratch block allocated
/// once per `Tcp.connect` call via `cabi_realloc(0, 0, 4, 64)`.
/// Cuts the per-call allocator traffic from 3-4 small bumps down
/// to a single 64-byte chunk and gives later phases stable offsets
/// to load from. Layout (every entry 16-aligned to leave slack):
///
/// | offset | size (B) | purpose                                |
/// |--------|----------|----------------------------------------|
/// | +0     | 8        | retptr — `resolve-addresses` result    |
/// | +16    | 24       | retptr — `resolve-next-address` result |
/// | +48    | 8        | retptr — `wasi:io/poll.poll` (out list) |
/// | +56    | 4        | input  — pollable handle for `poll`     |
///
/// Phases 4.2.2b1 only touches +0; b2 adds +16, +48, +56. The
/// block stays live for the entire helper invocation (Aver
/// `Tcp.connect` is one synchronous call), so no per-step
/// reallocation.
const SCRATCH_BLOCK_SIZE: i32 = 64;
const SCRATCH_OFFSET_RESOLVE: u32 = 0;
const SCRATCH_OFFSET_NEXT: u32 = 16;
const SCRATCH_OFFSET_POLL: u32 = 48;
const SCRATCH_OFFSET_POLLABLE_IN: u32 = 56;

pub(in crate::codegen::wasm_gc) fn emit_tcp_connect_stub(
    indices: &TcpConnectIndices,
    helpers: &TcpConnectHelperFns,
) -> Function {
    use wasm_encoder::{BlockType, MemArg};
    // Locals beyond the two params (0=host: ref string, 1=port: i64):
    //   local 2  = host_len (i32)         — bytes written by str_to_lm
    //   local 3  = scratch (i32)          — base of the 64-byte retptr block
    //   local 4  = resolve_stream (i32)   — handle from resolve-addresses Ok
    //   local 5  = pollable (i32)         — per-call subscribe handle
    //   local 6  = ipv4_a (i32)           ┐
    //   local 7  = ipv4_b (i32)           ├ first-IPv4 octets latched from
    //   local 8  = ipv4_c (i32)           │ resolve-next-address Ok+Some+ipv4
    //   local 9  = ipv4_d (i32)           ┘
    //   local 10 = socket (i32)           — tcp-socket handle from create-tcp-socket
    //   local 11 = in_stream (i32)        ┐
    //   local 12 = out_stream (i32)       ┘ unpacked from finish-connect Ok
    //   local 13 = stale_slot             — `ref null $tcp_slot` carrier used by the
    //                                       Phase 4.2.2e wraparound-drop branch.
    let stale_slot_ref = ValType::Ref(wasm_encoder::RefType {
        nullable: true,
        heap_type: wasm_encoder::HeapType::Concrete(helpers.pool.tcp_slot_type_idx),
    });
    // local 14 = saved_alloc (i32) — see Phase 4.2.2f bump-heap
    // rewind below. Lives next to the typed locals so every exit
    // point can restore the cursor.
    let mut f = Function::new(vec![
        (12u32, ValType::I32),
        (1u32, stale_slot_ref),
        // Phase 4.7+ fix #14 — `l_scan_idx`: pool-scan counter
        // for the live-count limit check. Distinct from
        // `tcp_next_id` (the monotonic id counter) because the
        // assigned slot index is no longer `tcp_next_id & 255` —
        // we walk the pool for the first non-busy slot.
        (1u32, ValType::I32),
    ]);
    let l_host_len: u32 = 2;
    let l_scratch: u32 = 3;
    let l_resolve_stream: u32 = 4;
    let l_pollable: u32 = 5;
    let l_ipv4_a: u32 = 6;
    let l_ipv4_b: u32 = 7;
    let l_ipv4_c: u32 = 8;
    let l_ipv4_d: u32 = 9;
    let l_socket: u32 = 10;
    let l_in_stream: u32 = 11;
    let l_out_stream: u32 = 12;
    let l_saved_alloc: u32 = 13;
    let l_stale_slot: u32 = 14;
    let l_scan_idx: u32 = 15;

    let mem4_resolve = MemArg {
        offset: u64::from(SCRATCH_OFFSET_RESOLVE),
        align: 2,
        memory_index: 0,
    };
    let mem4_resolve_off4 = MemArg {
        offset: u64::from(SCRATCH_OFFSET_RESOLVE + 4),
        align: 2,
        memory_index: 0,
    };
    let mem1_resolve = MemArg {
        offset: u64::from(SCRATCH_OFFSET_RESOLVE),
        align: 0,
        memory_index: 0,
    };
    // resolve-next-address retptr loads — all byte-granular (option,
    // variant, and ipv4 octet payloads sit at +2 / +4 / +6..+10).
    let mem1_next_outer = MemArg {
        offset: u64::from(SCRATCH_OFFSET_NEXT),
        align: 0,
        memory_index: 0,
    };
    let mem1_next_option = MemArg {
        offset: u64::from(SCRATCH_OFFSET_NEXT + 2),
        align: 0,
        memory_index: 0,
    };
    let mem1_next_variant = MemArg {
        offset: u64::from(SCRATCH_OFFSET_NEXT + 4),
        align: 0,
        memory_index: 0,
    };
    let mem1_next_octet_a = MemArg {
        offset: u64::from(SCRATCH_OFFSET_NEXT + 6),
        align: 0,
        memory_index: 0,
    };
    let mem1_next_octet_b = MemArg {
        offset: u64::from(SCRATCH_OFFSET_NEXT + 7),
        align: 0,
        memory_index: 0,
    };
    let mem1_next_octet_c = MemArg {
        offset: u64::from(SCRATCH_OFFSET_NEXT + 8),
        align: 0,
        memory_index: 0,
    };
    let mem1_next_octet_d = MemArg {
        offset: u64::from(SCRATCH_OFFSET_NEXT + 9),
        align: 0,
        memory_index: 0,
    };
    let mem4_pollable_in = MemArg {
        offset: u64::from(SCRATCH_OFFSET_POLLABLE_IN),
        align: 2,
        memory_index: 0,
    };

    // ── Phase 4.2.2f — bump-heap rewind prolog. ────────────────
    // Save the cabi_realloc cursor on entry; every exit path
    // (early Returns + the trailing fall-through) restores it
    // before leaving so the helper's transient scratch is
    // reclaimed. Without this Tcp.connect leaks ~64 bytes per call.
    f.instruction(&Instruction::GlobalGet(helpers.pool.bump_alloc_ptr_global));
    f.instruction(&Instruction::LocalSet(l_saved_alloc));

    // ── Phase 4.7+ fix #7 — port validation. ───────────────────
    // VM rejects `port < 0 || port > 65535` with a Tcp-prefixed Err
    // before any DNS or socket work happens. Wasip2 used to silently
    // I32WrapI64 and pass the truncated value into start-connect,
    // surfacing the failure as the generic `Result.Err("tcp: connect
    // failed")` (or worse, a successful connect to the wrong port).
    // Surface the same shape as VM up-front instead.
    f.instruction(&Instruction::LocalGet(1)); // port (i64)
    f.instruction(&Instruction::I64Const(0));
    f.instruction(&Instruction::I64LtS);
    f.instruction(&Instruction::LocalGet(1));
    f.instruction(&Instruction::I64Const(65535));
    f.instruction(&Instruction::I64GtS);
    f.instruction(&Instruction::I32Or);
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.port_err_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.port_err_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End);

    // ── Phase 4.2.2a — lazy network handle init. ───────────────
    // Pattern mirrors Console.print's stdout cache: if the global
    // is still the -1 sentinel, fetch the host network resource
    // handle once and stash it. The resource is program-lifetime
    // (wasmtime cleans up at component exit), so no drop helper.
    f.instruction(&Instruction::GlobalGet(helpers.dns.network_handle_global));
    f.instruction(&Instruction::I32Const(-1));
    f.instruction(&Instruction::I32Eq);
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::Call(helpers.dns.instance_network_fn));
    f.instruction(&Instruction::GlobalSet(helpers.dns.network_handle_global));
    f.instruction(&Instruction::End);

    // ── Phase 4.2.2b — shared scratch block + DNS resolve. ─────
    // Step 1 — marshal host into LM[0..host_len].
    f.instruction(&Instruction::LocalGet(0));
    f.instruction(&Instruction::Call(helpers.materialize.str_to_lm_fn));
    f.instruction(&Instruction::LocalSet(l_host_len));

    // Step 2 — allocate the shared 64-byte scratch block via
    // cabi_realloc (one bump per `Tcp.connect`, reused by every
    // retptr / pollable-input slot the body needs). Layout
    // documented next to `SCRATCH_BLOCK_SIZE` above.
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(4));
    f.instruction(&Instruction::I32Const(SCRATCH_BLOCK_SIZE));
    f.instruction(&Instruction::Call(helpers.materialize.cabi_realloc_fn));
    f.instruction(&Instruction::LocalSet(l_scratch));

    // Step 3 — resolve-addresses(network, host_ptr=0, host_len, retptr).
    // retptr lands at scratch[+0..+8].
    f.instruction(&Instruction::GlobalGet(helpers.dns.network_handle_global));
    f.instruction(&Instruction::I32Const(0)); // host_ptr (LM start)
    f.instruction(&Instruction::LocalGet(l_host_len));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::Call(helpers.dns.resolve_addresses_fn));

    // Step 4 — read the Result tag at scratch[+0]. On Err,
    // materialise "tcp: dns resolve failed" and return early; on
    // Ok, fall through to the (still-stubbed) tail.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_resolve));
    f.instruction(&Instruction::If(BlockType::Empty));
    // Err path: build the dns-error string + Result.Err + early return.
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.dns_err_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.dns_err_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End);

    // Step 5 — Ok branch: pull the stream handle out of scratch[+4].
    // Keep it live; the loop below pulls the first IPv4 address out
    // of it via subscribe/poll/resolve-next-address, then drops the
    // stream when the address is in our locals (or when the resolver
    // gave up before yielding one).
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load(mem4_resolve_off4));
    f.instruction(&Instruction::LocalSet(l_resolve_stream));
    let _ = mem4_resolve; // reserved for the future option / ipv4 octet loads

    // ── Phase 4.2.2b2 — DNS resolve loop, first-IPv4 wins. ─────
    // Block 1 — break target. Loop 0 — continue target. On a
    // successful IPv4 latch we `br 1` out with octets in locals;
    // on a None / IPv6 yield we `br 0` to keep polling; on a
    // resolver Err mid-stream we drop the stream and return Err.
    f.instruction(&Instruction::Block(BlockType::Empty));
    f.instruction(&Instruction::Loop(BlockType::Empty));

    // subscribe → pollable
    f.instruction(&Instruction::LocalGet(l_resolve_stream));
    f.instruction(&Instruction::Call(helpers.dns.stream_subscribe_fn));
    f.instruction(&Instruction::LocalSet(l_pollable));

    // Store pollable handle at scratch[+56] for the single-entry
    // `poll(in_ptr=scratch+56, in_len=1, retptr=scratch+48)` call.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::LocalGet(l_pollable));
    f.instruction(&Instruction::I32Store(mem4_pollable_in));

    // poll([pollable], 1, retptr). Result list is allocated by the
    // host into scratch[+48..+56]; we don't care which index fired
    // (the only pollable is ours).
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Const(SCRATCH_OFFSET_POLLABLE_IN as i32));
    f.instruction(&Instruction::I32Add);
    f.instruction(&Instruction::I32Const(1));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Const(SCRATCH_OFFSET_POLL as i32));
    f.instruction(&Instruction::I32Add);
    f.instruction(&Instruction::Call(helpers.dns.poll_fn));

    // drop pollable — single-use per iteration.
    f.instruction(&Instruction::LocalGet(l_pollable));
    f.instruction(&Instruction::Call(helpers.dns.drop_pollable_fn));

    // resolve-next-address(stream, retptr=scratch+16).
    f.instruction(&Instruction::LocalGet(l_resolve_stream));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Const(SCRATCH_OFFSET_NEXT as i32));
    f.instruction(&Instruction::I32Add);
    f.instruction(&Instruction::Call(helpers.dns.resolve_next_address_fn));

    // outer tag — on Err, drop stream + return dns-no-addr Err.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_next_outer));
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::LocalGet(l_resolve_stream));
    f.instruction(&Instruction::Call(helpers.dns.drop_resolve_stream_fn));
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.no_addr_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.no_addr_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End);

    // option tag — None means the resolver has exhausted the stream
    // (WASI spec: would-block surfaces as `Err(error-code::would-block)`
    // on the outer arm, not as `Ok(None)`). For an IPv6-only host
    // we'd already have looped past every Some(ipv6) below; reaching
    // Ok(None) means no IPv4 will ever come, so we drop the stream
    // and surface `tcp: dns no addresses`. Treating None as "poll
    // again" (the previous behaviour) spins forever on those hosts.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_next_option));
    f.instruction(&Instruction::I32Eqz);
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::LocalGet(l_resolve_stream));
    f.instruction(&Instruction::Call(helpers.dns.drop_resolve_stream_fn));
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.no_addr_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.no_addr_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End);

    // ip-address variant tag — IPv6 (tag != 0) skipped today, loop
    // to look for the next yield. IPv4 falls through.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_next_variant));
    f.instruction(&Instruction::BrIf(0));

    // IPv4 latch — read 4 octets out of scratch[+22..+26] and stash.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_next_octet_a));
    f.instruction(&Instruction::LocalSet(l_ipv4_a));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_next_octet_b));
    f.instruction(&Instruction::LocalSet(l_ipv4_b));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_next_octet_c));
    f.instruction(&Instruction::LocalSet(l_ipv4_c));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_next_octet_d));
    f.instruction(&Instruction::LocalSet(l_ipv4_d));

    // Break out of the loop — first IPv4 wins.
    f.instruction(&Instruction::Br(1));

    f.instruction(&Instruction::End); // loop
    f.instruction(&Instruction::End); // block

    // Stream's job is done — drop it. The octets in l_ipv4_* feed
    // start-connect below.
    f.instruction(&Instruction::LocalGet(l_resolve_stream));
    f.instruction(&Instruction::Call(helpers.dns.drop_resolve_stream_fn));

    // ── Phase 4.7+ fix #10 — pool slot reservation. ─────────────
    //
    // Lazy-init `tcp_pool` on the first connect, then refuse if
    // the target slot already holds a live (`in_use == 1`)
    // occupant. Previously this branch ran AFTER finish-connect
    // and force-dropped the live slot to make room; aver-rt::tcp
    // (VM / self-host / wasm-gc-bridge) refuses with `Err("Tcp.
    // connect: connection limit reached (256 max)")`. Match that
    // here, refusing BEFORE we burn a fresh socket + connect on
    // an address we'd have to immediately unwind.
    f.instruction(&Instruction::GlobalGet(helpers.pool.tcp_pool_global));
    f.instruction(&Instruction::RefIsNull);
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::I32Const(256));
    f.instruction(&Instruction::ArrayNewDefault(
        helpers.pool.tcp_pool_type_idx,
    ));
    f.instruction(&Instruction::GlobalSet(helpers.pool.tcp_pool_global));
    f.instruction(&Instruction::End);

    // Phase 4.7+ fix #14 — find the first non-busy pool slot.
    // `aver-rt::tcp::connect` rejects only when total live count
    // hits 256; modulo-of-counter would falsely reject a fresh
    // connect whenever the wraparound slot happened to be in use,
    // even with free slots elsewhere in the pool.
    //
    // Scan slots 0..256. Slot is reusable if null (never claimed)
    // or in_use == 0 (closed). First match → break out of $found
    // block carrying the slot index in `l_scan_idx`. If we walk
    // off the end with no match, the loop falls through to the
    // limit Err.
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::LocalSet(l_scan_idx));
    f.instruction(&Instruction::Block(BlockType::Empty)); // $found
    f.instruction(&Instruction::Loop(BlockType::Empty));
    f.instruction(&Instruction::LocalGet(l_scan_idx));
    f.instruction(&Instruction::I32Const(256));
    f.instruction(&Instruction::I32GeU);
    f.instruction(&Instruction::If(BlockType::Empty));
    // No free slot — emit limit Err.
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.limit_err_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.limit_err_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End); // i >= 256 if
    f.instruction(&Instruction::GlobalGet(helpers.pool.tcp_pool_global));
    f.instruction(&Instruction::LocalGet(l_scan_idx));
    f.instruction(&Instruction::ArrayGet(helpers.pool.tcp_pool_type_idx));
    f.instruction(&Instruction::LocalTee(l_stale_slot));
    f.instruction(&Instruction::RefIsNull);
    f.instruction(&Instruction::BrIf(1)); // null slot → break $found
    f.instruction(&Instruction::LocalGet(l_stale_slot));
    f.instruction(&Instruction::StructGet {
        struct_type_index: helpers.pool.tcp_slot_type_idx,
        field_index: 3, // in_use
    });
    f.instruction(&Instruction::I32Eqz);
    f.instruction(&Instruction::BrIf(1)); // closed slot → break $found
    f.instruction(&Instruction::LocalGet(l_scan_idx));
    f.instruction(&Instruction::I32Const(1));
    f.instruction(&Instruction::I32Add);
    f.instruction(&Instruction::LocalSet(l_scan_idx));
    f.instruction(&Instruction::Br(0)); // continue loop
    f.instruction(&Instruction::End); // loop
    f.instruction(&Instruction::End); // $found block — l_scan_idx is the slot

    // ── Phase 4.2.2c — create-tcp-socket + start/finish-connect. ─
    // Scratch offset +0..+8 is now free (DNS exit); reuse for the
    // socket-side retptrs:
    //   +0..+8 (create-tcp-socket result: tag@0 + handle/err@4)
    //   +0..+2 (start-connect result:     tag@0 + err@1)
    //   +0..+12 (finish-connect result:   tag@0 + (in@4, out@8) | err@4)
    // All three calls run sequentially and each fully consumes the
    // previous retptr's data before issuing the next.

    // Step 6 — create-tcp-socket(family=0=ipv4, retptr=scratch+0).
    f.instruction(&Instruction::I32Const(0)); // ip-address-family.ipv4
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::Call(helpers.socket.create_tcp_socket_fn));

    // tag@scratch+0; Err ⇒ socket-create Err.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_resolve));
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.sock_err_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.sock_err_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End);

    // Latch socket handle at scratch+4.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load(mem4_resolve_off4));
    f.instruction(&Instruction::LocalSet(l_socket));

    // Step 7 — start-connect(socket, network, ipv4_socket_address,
    //                        retptr). 14 i32 args + retptr (15 total).
    // ip-socket-address flat:
    //   pos 0:  variant tag    (0 = ipv4)
    //   pos 1:  port           (u16 → i32, narrowing from Aver Int)
    //   pos 2:  octet a        (u8 → i32)
    //   pos 3:  octet b
    //   pos 4:  octet c
    //   pos 5:  octet d
    //   pos 6..11: 0×6         (ipv6 padding — unused for ipv4)
    f.instruction(&Instruction::LocalGet(l_socket));
    f.instruction(&Instruction::GlobalGet(helpers.dns.network_handle_global));
    f.instruction(&Instruction::I32Const(0)); // addr variant: ipv4
    f.instruction(&Instruction::LocalGet(1)); // port (i64 param)
    f.instruction(&Instruction::I32WrapI64);
    f.instruction(&Instruction::LocalGet(l_ipv4_a));
    f.instruction(&Instruction::LocalGet(l_ipv4_b));
    f.instruction(&Instruction::LocalGet(l_ipv4_c));
    f.instruction(&Instruction::LocalGet(l_ipv4_d));
    // 6× zero pads for the ipv6 join positions.
    for _ in 0..6 {
        f.instruction(&Instruction::I32Const(0));
    }
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::Call(helpers.socket.start_connect_fn));

    // start-connect retptr is 2 bytes: tag@0 + err@1. Err ⇒ drop
    // socket and surface conn-failed.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_resolve));
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::LocalGet(l_socket));
    f.instruction(&Instruction::Call(helpers.socket.drop_tcp_socket_fn));
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.conn_err_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.conn_err_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End);

    // Step 8 — socket.subscribe + poll([pollable], 1, retptr=scratch+48).
    f.instruction(&Instruction::LocalGet(l_socket));
    f.instruction(&Instruction::Call(helpers.socket.socket_subscribe_fn));
    f.instruction(&Instruction::LocalSet(l_pollable));

    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::LocalGet(l_pollable));
    f.instruction(&Instruction::I32Store(mem4_pollable_in));

    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Const(SCRATCH_OFFSET_POLLABLE_IN as i32));
    f.instruction(&Instruction::I32Add);
    f.instruction(&Instruction::I32Const(1));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Const(SCRATCH_OFFSET_POLL as i32));
    f.instruction(&Instruction::I32Add);
    f.instruction(&Instruction::Call(helpers.dns.poll_fn));

    f.instruction(&Instruction::LocalGet(l_pollable));
    f.instruction(&Instruction::Call(helpers.dns.drop_pollable_fn));

    // Step 9 — finish-connect(socket, retptr=scratch+0). Retptr 12B:
    //   +0: tag, +4: on Ok in_stream / on Err error-code, +8: out_stream.
    f.instruction(&Instruction::LocalGet(l_socket));
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::Call(helpers.socket.finish_connect_fn));

    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load8U(mem1_resolve));
    f.instruction(&Instruction::If(BlockType::Empty));
    f.instruction(&Instruction::LocalGet(l_socket));
    f.instruction(&Instruction::Call(helpers.socket.drop_tcp_socket_fn));
    f.instruction(&Instruction::I32Const(0));
    f.instruction(&Instruction::I32Const(indices.conn_err_len as i32));
    f.instruction(&Instruction::ArrayNewData {
        array_type_index: indices.string_type_idx,
        array_data_index: indices.conn_err_segment_idx,
    });
    f.instruction(&Instruction::Call(helpers.materialize.result_err_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    f.instruction(&Instruction::Return);
    f.instruction(&Instruction::End);

    // Unpack the (input-stream, output-stream) tuple from
    // scratch+4 and scratch+8. These plus l_socket are the three
    // i32 handles Phase 4.2.2d will stash into the pool slot.
    f.instruction(&Instruction::LocalGet(l_scratch));
    f.instruction(&Instruction::I32Load(mem4_resolve_off4));
    f.instruction(&Instruction::LocalSet(l_in_stream));
    // out_stream lives at scratch+8 — same align-2 i32 load as +4
    // with a +4-offset variant of mem4_resolve_off4.
    {
        let mem4_resolve_off8 = MemArg {
            offset: u64::from(SCRATCH_OFFSET_RESOLVE + 8),
            align: 2,
            memory_index: 0,
        };
        f.instruction(&Instruction::LocalGet(l_scratch));
        f.instruction(&Instruction::I32Load(mem4_resolve_off8));
        f.instruction(&Instruction::LocalSet(l_out_stream));
    }

    // Octets aren't used past this point — they fed start-connect
    // already. Keep the local declarations stable for future hooks.
    let _ = (l_ipv4_a, l_ipv4_b, l_ipv4_c, l_ipv4_d);

    // ── Phase 4.2.2d — pool slot + Tcp.Connection materialise. ─
    //
    // Pool limit + lazy-init was moved up-front (right after the
    // DNS resolve drop, before create-tcp-socket) by Phase 4.7+
    // fix #10 so the 257th live connect refuses with
    // `Err("tcp: connection limit reached (256 max)")` instead of
    // silently evicting the existing live occupant of slot
    // `tcp_next_id & 255`. By the time we reach Step 11 the slot
    // is guaranteed null or `in_use == 0`, so `struct.new` +
    // `array.set` overwrite the entry safely.
    let _ = l_stale_slot; // local kept for ABI stability; no body usage

    // Step 11 — `tcp_pool[l_scan_idx] = struct.new $tcp_slot
    // (socket, in_stream, out_stream, in_use=1, id_value=tcp_next_id)`.
    // `l_scan_idx` is the slot the Phase 4.7+ fix #14 scan picked
    // (first free index in pool order); `id_value` stays the
    // monotonic counter so close/write/read can match a specific
    // generation rather than just an array index.
    f.instruction(&Instruction::GlobalGet(helpers.pool.tcp_pool_global));
    f.instruction(&Instruction::LocalGet(l_scan_idx));
    f.instruction(&Instruction::LocalGet(l_socket));
    f.instruction(&Instruction::LocalGet(l_in_stream));
    f.instruction(&Instruction::LocalGet(l_out_stream));
    f.instruction(&Instruction::I32Const(1)); // in_use = 1
    f.instruction(&Instruction::GlobalGet(helpers.pool.tcp_next_id_global));
    f.instruction(&Instruction::StructNew(helpers.pool.tcp_slot_type_idx));
    f.instruction(&Instruction::ArraySet(helpers.pool.tcp_pool_type_idx));

    // Step 12 — `id = __rt_tcp_format_id(tcp_next_id)`. Full counter
    // value, not masked — the id string is the program-level handle
    // and must be unique across the lifetime of the process. Parse
    // recovers the same i32 via `__rt_tcp_parse_id`; consumers then
    // compare it against the stored `slot.id_value` to detect stale
    // references after pool wraparound.
    f.instruction(&Instruction::GlobalGet(helpers.pool.tcp_next_id_global));
    f.instruction(&Instruction::Call(helpers.materialize.format_id_fn));

    // Step 13 — bump `tcp_next_id`. Wrapping i32 add at counter
    // level is fine: the slot picker masks back to 0..255, and the
    // id_value freshness check tolerates wrap because two slots
    // 2^32 connects apart can't both have live references in any
    // realistic program.
    f.instruction(&Instruction::GlobalGet(helpers.pool.tcp_next_id_global));
    f.instruction(&Instruction::I32Const(1));
    f.instruction(&Instruction::I32Add);
    f.instruction(&Instruction::GlobalSet(helpers.pool.tcp_next_id_global));

    // Step 14 — build `Tcp.Connection { id, host, port }` via the
    // existing factory. Stack: [id]; push host (param 0) and port
    // (param 1); the factory pops all three and returns the
    // record ref.
    f.instruction(&Instruction::LocalGet(0)); // host
    f.instruction(&Instruction::LocalGet(1)); // port (i64)
    f.instruction(&Instruction::Call(helpers.materialize.record_make_fn));

    // Step 15 — wrap in `Result.Ok(conn)` and let the function
    // return naturally. The stub-err tail is gone — every exit out
    // of the helper now goes through a typed factory. Restore the
    // bump-heap cursor first so the per-call scratch / retptrs we
    // allocated through `cabi_realloc` are reclaimed.
    f.instruction(&Instruction::Call(helpers.materialize.result_ok_fn));
    restore_bump(&mut f, l_saved_alloc, helpers.pool.bump_alloc_ptr_global);
    let _ = indices.stub_err_segment_idx;
    let _ = indices.stub_err_len;
    f.instruction(&Instruction::End);
    f
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn stub_emit_compiles() {
        // Smoke test — the body emitter must produce a well-formed
        // wasm Function with no panics on synthetic indices. Actual
        // wasm validation runs at module-emit time.
        let indices = TcpConnectIndices {
            fn_type: 0,
            fn_idx: 0,
            string_type_idx: 1,
            stub_err_segment_idx: 0,
            stub_err_len: b"tcp: connect not yet implemented".len() as u32,
            dns_err_segment_idx: 1,
            dns_err_len: b"tcp: dns resolve failed".len() as u32,
            no_addr_segment_idx: 2,
            no_addr_len: b"tcp: dns no addresses".len() as u32,
            sock_err_segment_idx: 3,
            sock_err_len: b"tcp: socket create failed".len() as u32,
            conn_err_segment_idx: 4,
            conn_err_len: b"tcp: connect failed".len() as u32,
            port_err_segment_idx: 5,
            port_err_len: b"tcp: port out of range".len() as u32,
            limit_err_segment_idx: 6,
            limit_err_len: b"tcp: connection limit reached (256 max)".len() as u32,
        };
        let helpers = TcpConnectHelperFns {
            pool: TcpConnectPool {
                tcp_next_id_global: 1,
                tcp_pool_global: 2,
                tcp_slot_type_idx: 3,
                tcp_pool_type_idx: 4,
                bump_alloc_ptr_global: 5,
            },
            dns: TcpConnectDns {
                instance_network_fn: 3,
                network_handle_global: 0,
                resolve_addresses_fn: 6,
                resolve_next_address_fn: 11,
                drop_resolve_stream_fn: 7,
                stream_subscribe_fn: 8,
                poll_fn: 9,
                drop_pollable_fn: 10,
            },
            socket: TcpConnectSocket {
                create_tcp_socket_fn: 12,
                start_connect_fn: 13,
                finish_connect_fn: 15,
                socket_subscribe_fn: 14,
                drop_tcp_socket_fn: 16,
            },
            materialize: TcpConnectMaterialize {
                cabi_realloc_fn: 4,
                str_to_lm_fn: 5,
                format_id_fn: 17,
                record_make_fn: 18,
                result_ok_fn: 19,
                result_err_fn: 2,
            },
        };
        let _f = emit_tcp_connect_stub(&indices, &helpers);
    }
}