-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodegen.ml
More file actions
2935 lines (2696 loc) · 125 KB
/
Copy pathcodegen.ml
File metadata and controls
2935 lines (2696 loc) · 125 KB
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
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(* SPDX-License-Identifier: MPL-2.0 *)
(* SPDX-FileCopyrightText: 2024-2025 hyperpolymath *)
(** WebAssembly code generation from AffineScript AST.
This module translates type-checked and borrow-checked AffineScript
programs into WebAssembly modules.
*)
open Ast
open Wasm
(** Ownership kind for typed-wasm schema annotations.
Maps AffineScript ownership qualifiers to typed-wasm Level 7/10 verification. *)
type ownership_kind =
| Unrestricted (** Plain value, no ownership constraint (Wasm i32/f64 etc.) *)
| Linear (** TyOwn / own — consumed exactly once (typed-wasm Level 10 linearity) *)
| SharedBorrow (** TyRef / ref — read-only aliasing safety (typed-wasm Level 7) *)
| ExclBorrow (** TyMut / mut — exclusive mutable aliasing safety (typed-wasm Level 7) *)
(** Code generation context *)
type context = {
types : func_type list; (** type section *)
funcs : func list; (** function definitions *)
exports : export list; (** exports *)
imports : import list; (** imports *)
globals : global list; (** global variables *)
locals : (string * int) list; (** local variable name to index map *)
next_local : int; (** next available local index *)
loop_depth : int; (** current loop nesting depth *)
func_indices : (string * int) list;
(** Top-level name environment shared by functions and constants.
- [k >= 0]: Wasm function index (imports + defined functions).
Populated by both [TopFn] (defined function) and
[TopFn _ with fd_body = FnExtern] (host-supplied import).
- [k < 0]: Constant (global): actual global index is [-(k+1)].
Populated by [TopConst].
Entries are inserted in source declaration order by [gen_decl]. *)
lambda_funcs : func list; (** lifted lambda functions *)
next_lambda_id : int; (** next lambda function ID *)
heap_ptr : int option; (** global index for heap pointer, if initialized *)
field_layouts : (string * (string * int) list) list; (** variable name -> [(field, offset)] *)
struct_layouts : (string * (string * int) list) list;
(** Struct type name -> [(field, offset)]. Registered from TopType(TyStruct)
at decl time so function-parameter and call-result field accesses can
recover the field layout by type, not by let-binding shape. *)
fn_ret_structs : (string * string) list;
(** Function name -> struct type name it returns (if any). Lets a
`let s = make()` call site register s's field layout in field_layouts
when the callee's return type is a known struct. *)
variant_tags : (string * int) list; (** constructor name -> tag (int) *)
string_data : (string * int) list; (** string content -> memory offset *)
next_string_offset : int; (** next available offset for string data *)
datas : data list; (** data segments *)
ownership_annots : (int * ownership_kind list * ownership_kind) list;
(** Collected ownership annotations: (func_index, param_kinds, return_kind).
Emitted as the [typedwasm.ownership] Wasm custom section for typed-wasm
Level 7/10 verification. Kind encoding: 0=Unrestricted, 1=Linear, 2=SharedBorrow, 3=ExclBorrow. *)
wasi_func_indices : (string * int) list;
(** ADR-015 S4 (#180): WASI preview1 import name → wasm func index.
Populated at module-assembly time from the optional-imports
pre-scan. `fd_write` is always present at 0; other entries
(`clock_time_get`, `environ_sizes_get`, `args_sizes_get`, …) are
added on-demand in a canonical order, with indices computed by
position. WASI builtin special-cases look up their own import
index by name from this map. *)
}
(** Code generation error *)
type codegen_error =
| UnsupportedFeature of string
| UnboundVariable of string
| TypeMismatch of string
[@@deriving show]
type 'a result = ('a, codegen_error) Result.t
(** Result bind operator *)
let ( let* ) = Result.bind
(** Count imported functions (for index offsets) *)
let import_func_count (ctx : context) : int =
List.fold_left (fun acc imp ->
match imp.i_desc with
| ImportFunc _ -> acc + 1
| _ -> acc
) 0 ctx.imports
(** Create initial context *)
let create_context () : context = {
types = [];
funcs = [];
exports = [];
imports = [];
globals = [];
locals = [];
next_local = 0;
loop_depth = 0;
func_indices = [];
lambda_funcs = [];
next_lambda_id = 0;
heap_ptr = None;
field_layouts = [];
struct_layouts = [];
fn_ret_structs = [];
variant_tags = [];
string_data = [];
next_string_offset = 2048; (* Start strings after heap at offset 2048 *)
datas = [];
ownership_annots = [];
wasi_func_indices = [];
}
(** Extract ownership kind from a parameter declaration.
Checks p_ownership first; falls back to the shape of p_ty. *)
let ownership_kind_of_param (p : param) : ownership_kind =
match p.p_ownership with
| Some Own -> Linear
| Some Ref -> SharedBorrow
| Some Mut -> ExclBorrow
| None ->
match p.p_ty with
| TyOwn _ -> Linear
| TyRef _ -> SharedBorrow
| TyMut _ -> ExclBorrow
| _ -> Unrestricted
(** If [ty] names a known struct (through any number of own/ref/mut wrappers),
return that struct's name. Lets us recover a struct's field layout from
parameter and return-type annotations so `.field_N` reads use the correct
offset instead of defaulting to 0. *)
let rec struct_name_of_ty (ty : type_expr) : string option =
match ty with
| TyCon id -> Some id.name
| TyApp (id, _) -> Some id.name
| TyOwn inner | TyRef inner | TyMut inner -> struct_name_of_ty inner
| _ -> None
(** Extract ownership kind from an optional return type expression *)
let ownership_kind_of_ret (ret : type_expr option) : ownership_kind =
match ret with
| Some (TyOwn _) -> Linear
| Some (TyRef _) -> SharedBorrow
| Some (TyMut _) -> ExclBorrow
| _ -> Unrestricted
(** Encode an ownership_kind as a single byte (0–3) *)
let ownership_kind_byte = function
| Unrestricted -> 0 | Linear -> 1 | SharedBorrow -> 2 | ExclBorrow -> 3
(** Build the payload for the [typedwasm.ownership] Wasm custom section.
Encoding (all little-endian):
u32 entry_count
per entry:
u32 func_index
u8 param_count
u8* param_kind (one per param, see kind encoding above)
u8 return_kind *)
let build_ownership_section (annots : (int * ownership_kind list * ownership_kind) list) : bytes =
Tw_section.Encode.ownership
(List.map (fun (func_idx, param_kinds, ret_kind) ->
(func_idx,
List.map ownership_kind_byte param_kinds,
ownership_kind_byte ret_kind))
annots)
(** Map AffineScript type to WASM value type *)
let type_to_wasm (ty : type_expr) : value_type result =
match ty with
| TyCon id when id.name = "Float" -> Ok F64
| TyCon id when id.name = "Bool" -> Ok I32
| TyCon id when id.name = "Int" -> Ok I32
| TyCon id when id.name = "Char" -> Ok I32
| TyCon id when id.name = "String" -> Ok I32 (* pointer to heap *)
| TyCon id when id.name = "Nat" -> Ok I32
| TyCon _ -> Ok I32 (* default for user types — heap pointer *)
| TyApp _ | TyTuple _ | TyRecord _ | TyArrow _ -> Ok I32
| _ -> Ok I32 (* conservative default *)
(** Allocate a new local variable *)
let alloc_local (ctx : context) (name : string) : (context * int) =
let idx = ctx.next_local in
let locals' = (name, idx) :: ctx.locals in
({ ctx with locals = locals'; next_local = idx + 1 }, idx)
(** Look up local variable index *)
let lookup_local (ctx : context) (name : string) : int result =
match List.assoc_opt name ctx.locals with
| Some idx -> Ok idx
| None -> Error (UnboundVariable name)
(** Generate code to bind a pattern to the value on the WASM stack.
Assumes the RHS value is already on the stack.
Returns instructions that consume the stack value and bind locals. *)
let rec gen_pattern_bind (ctx : context) (pat : pattern) : (context * instr list) result =
match pat with
| PatVar id ->
let (ctx', idx) = alloc_local ctx id.name in
Ok (ctx', [LocalSet idx])
| PatWildcard _ ->
(* Discard the value *)
Ok (ctx, [Drop])
| PatTuple pats ->
(* Value is a heap pointer to the tuple. Store it in a temp, then
load each element at its offset and bind the sub-pattern. *)
let (ctx', tmp_idx) = alloc_local ctx "__tuple_tmp" in
let n = List.length pats in
let* (ctx_final, elem_codes) = List.fold_left (fun acc (i, sub_pat) ->
let* (c, codes) = acc in
(* Load tuple element: memory[tmp + i*4] *)
let load_code = [
LocalGet tmp_idx;
I32Const (Int32.of_int (i * 4));
I32Add;
I32Load (2, 0);
] in
let* (c', bind_code) = gen_pattern_bind c sub_pat in
Ok (c', codes @ load_code @ bind_code)
) (Ok (ctx', [])) (List.mapi (fun i p -> (i, p)) pats) in
let _ = n in
Ok (ctx_final, [LocalSet tmp_idx] @ elem_codes)
| PatAs (id, sub_pat) ->
(* Bind the whole value to id, then also match sub-pattern *)
let (ctx', idx) = alloc_local ctx id.name in
(* Duplicate value: store to local, get it back for sub-pattern *)
let* (ctx'', sub_code) = gen_pattern_bind ctx' sub_pat in
Ok (ctx'', [LocalTee idx] @ sub_code)
| _ ->
(* Other patterns (literals, constructors, records, or) need runtime
checking which is complex in WASM. For now, treat as a variable
bind of the whole value with a generated name. *)
let (ctx', idx) = alloc_local ctx "__pat_bind" in
Ok (ctx', [LocalSet idx])
(** Ensure heap pointer global is initialized.
Returns (context, heap_global_idx). *)
let ensure_heap_ptr (ctx : context) : (context * int) =
match ctx.heap_ptr with
| Some idx -> (ctx, idx)
| None ->
(* Create heap pointer global initialized to 1024 (1KB) *)
let idx = List.length ctx.globals in
let heap_global = {
g_type = I32;
g_mutable = true;
g_init = [I32Const 1024l]; (* Start heap at 1KB *)
} in
({ ctx with
globals = ctx.globals @ [heap_global];
heap_ptr = Some idx }, idx)
(** Generate code to allocate memory on the heap.
Returns instructions that leave the allocated address on the stack.
size_in_bytes: number of bytes to allocate *)
let gen_heap_alloc (ctx : context) (size_in_bytes : int) : (context * instr list) =
let (ctx', heap_idx) = ensure_heap_ptr ctx in
(* Get current heap pointer, then increment it *)
let alloc_code = [
GlobalGet heap_idx; (* Get current heap address *)
GlobalGet heap_idx; (* Get it again *)
I32Const (Int32.of_int size_in_bytes); (* Size to allocate *)
I32Add; (* Calculate new heap pointer *)
GlobalSet heap_idx; (* Update heap pointer *)
(* Stack now has the allocated address *)
] in
(ctx', alloc_code)
(** Find free variables in an expression.
Returns list of variable names that are used but not bound within the expression.
bound_vars: variables already bound in enclosing scope (parameters, let bindings) *)
let rec find_free_vars (bound_vars : string list) (expr : expr) : string list =
match expr with
| ExprLit _ -> []
| ExprVar id ->
if List.mem id.name bound_vars then [] else [id.name]
| ExprBinary (e1, _, e2) ->
find_free_vars bound_vars e1 @ find_free_vars bound_vars e2
| ExprUnary (_, e) ->
find_free_vars bound_vars e
| ExprIf ei ->
find_free_vars bound_vars ei.ei_cond @
find_free_vars bound_vars ei.ei_then @
(match ei.ei_else with
| Some e -> find_free_vars bound_vars e
| None -> [])
| ExprLet lb ->
let rhs_free = find_free_vars bound_vars lb.el_value in
(* Add bound variable to scope for body *)
let new_bound = match lb.el_pat with
| PatVar id -> id.name :: bound_vars
| _ -> bound_vars
in
let body_free = match lb.el_body with
| Some e -> find_free_vars new_bound e
| None -> []
in
rhs_free @ body_free
| ExprLambda lam ->
(* Parameters are bound within lambda *)
let param_names = List.map (fun p -> p.p_name.name) lam.elam_params in
find_free_vars (param_names @ bound_vars) lam.elam_body
| ExprApp (f, args) ->
find_free_vars bound_vars f @
List.concat (List.map (find_free_vars bound_vars) args)
| ExprBlock blk ->
(* Statements may introduce bindings *)
let (bound_after, free) = List.fold_left (fun (bound, acc_free) stmt ->
match stmt with
| StmtLet sl ->
let rhs_free = find_free_vars bound sl.sl_value in
let new_bound = match sl.sl_pat with
| PatVar id -> id.name :: bound
| _ -> bound
in
(new_bound, acc_free @ rhs_free)
| StmtExpr e ->
(bound, acc_free @ find_free_vars bound e)
| _ -> (bound, acc_free)
) (bound_vars, []) blk.blk_stmts in
(* The tail expression is in scope of the block's own `let`
bindings, so its free vars must exclude them — use the
threaded [bound_after], not the original [bound_vars]. (Prior
code used [bound_vars], spuriously reporting block-local
binders as free; surfaced by #225 PR3c chained continuations.) *)
let expr_free = match blk.blk_expr with
| Some e -> find_free_vars bound_after e
| None -> []
in
free @ expr_free
| ExprMatch m ->
find_free_vars bound_vars m.em_scrutinee @
List.concat (List.map (fun arm -> find_free_vars bound_vars arm.ma_body) m.em_arms)
| ExprReturn e_opt ->
(match e_opt with Some e -> find_free_vars bound_vars e | None -> [])
| ExprTuple exprs | ExprArray exprs ->
List.concat (List.map (find_free_vars bound_vars) exprs)
| ExprRecord r ->
List.concat (List.map (fun (_, e_opt) ->
match e_opt with
| Some e -> find_free_vars bound_vars e
| None -> []
) r.er_fields)
| ExprField (e, _) -> find_free_vars bound_vars e
| ExprTupleIndex (e, _) -> find_free_vars bound_vars e
| ExprIndex (e1, e2) ->
find_free_vars bound_vars e1 @ find_free_vars bound_vars e2
| ExprVariant _ -> []
| ExprSpan (e, _) -> find_free_vars bound_vars e
| _ -> [] (* Other expressions *)
(** Remove duplicates from list *)
let dedup (lst : string list) : string list =
List.fold_left (fun acc x ->
if List.mem x acc then acc else x :: acc
) [] lst |> List.rev
(** Generate code for a literal *)
let gen_literal (ctx : context) (lit : literal) : (context * instr) result =
match lit with
| LitUnit _ -> Ok (ctx, I32Const 0l) (* Unit represented as 0 *)
| LitBool (b, _) -> Ok (ctx, I32Const (if b then 1l else 0l))
| LitInt (n, _) -> Ok (ctx, I32Const (Int32.of_int n))
| LitFloat (f, _) -> Ok (ctx, F64Const f)
| LitChar (c, _) -> Ok (ctx, I32Const (Int32.of_int (Char.code c)))
| LitString (s, _) ->
(* Check if string already exists *)
begin match List.assoc_opt s ctx.string_data with
| Some offset ->
(* String already in memory, return pointer *)
Ok (ctx, I32Const (Int32.of_int offset))
| None ->
(* Add new string to data section *)
let offset = ctx.next_string_offset in
let str_bytes = Bytes.of_string s in
let str_len = Bytes.length str_bytes in
(* String layout: [length: i32][...utf8 bytes...] *)
let len_bytes = Bytes.create 4 in
Bytes.set_int32_le len_bytes 0 (Int32.of_int str_len);
let full_data = Bytes.cat len_bytes str_bytes in
let data_segment = { d_data = full_data; d_offset = offset } in
let ctx' = {
ctx with
string_data = (s, offset) :: ctx.string_data;
next_string_offset = offset + 4 + str_len;
datas = data_segment :: ctx.datas;
} in
Ok (ctx', I32Const (Int32.of_int offset))
end
(** Generate code for binary operation *)
let gen_binop (op : binary_op) : instr =
match op with
| OpAdd -> I32Add
| OpSub -> I32Sub
| OpMul -> I32Mul
| OpDiv -> I32DivS
| OpMod -> I32RemS
| OpEq -> I32Eq
| OpNe -> I32Ne
| OpLt -> I32LtS
| OpLe -> I32LeS
| OpGt -> I32GtS
| OpGe -> I32GeS
| OpAnd -> I32And
| OpOr -> I32Or
| OpBitAnd -> I32And
| OpBitOr -> I32Or
| OpBitXor -> I32Xor
| OpShl -> I32Shl
| OpShr -> I32ShrS
| OpConcat -> I32Add (* Placeholder *)
(** Generate code for unary operation *)
let gen_unop (op : unary_op) : instr result =
match op with
| OpNeg -> Ok I32Sub (* 0 - x *)
| OpNot -> Ok I32Eqz (* x == 0 *)
| OpBitNot -> Ok I32Xor (* -1 ^ x *)
| OpRef | OpMutRef -> Error (UnsupportedFeature "OpRef/OpMutRef handled in ExprUnary")
| OpDeref -> Error (UnsupportedFeature "OpDeref handled in ExprUnary")
(** ADR-013 #225 PR3c — recursive CPS hook. The async-boundary transform
([detect_async_base_case] + [gen_async_base_case]) is defined below
[gen_expr] but must be reachable from *inside* the continuation
lambda's body generation so that a continuation which is itself an
async boundary is transformed too (Async→Async chaining). A forward
reference, populated once at module init, breaks the definition-order
cycle without relocating the whole transform into the rec group.
Returns [Some result] when [expr] matched the async shape (and
`thenableThen` is importable), else [None] ⇒ caller lowers normally.
Recursion terminates: each application peels exactly one async
boundary off a finite, strictly-smaller continuation. *)
let async_transform_hook
: (context -> expr -> (context * instr list) result option) ref
= ref (fun _ _ -> None)
(** Generate code for an expression, returning instructions and updated context *)
let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
match expr with
| ExprLit lit ->
let* (ctx', instr) = gen_literal ctx lit in
Ok (ctx', [instr])
| ExprVar id ->
begin match lookup_local ctx id.name with
| Ok idx -> Ok (ctx, [LocalGet idx])
| Error _ ->
(* Fallback: bare identifier that names a zero-arity enum variant.
Matches the ExprCall resolution at line 658 so that both
`Initialised` and `Initialised()` work as expressions when the
name is known as a variant constructor. Without this, a match
arm body of the form `Uninitialised => Initialised` fails with
UnboundVariable even though the parser accepts it. *)
begin match List.assoc_opt id.name ctx.variant_tags with
| Some tag -> Ok (ctx, [I32Const (Int32.of_int tag)])
| None ->
(* Top-level const bindings are stored in func_indices with a
negative sentinel: actual global index = -(k+1). *)
begin match List.assoc_opt id.name ctx.func_indices with
| Some k when k < 0 -> Ok (ctx, [GlobalGet (-(k + 1))])
| _ -> Error (UnboundVariable id.name)
end
end
end
| ExprBinary (left, OpConcat, right) ->
(* List concatenation `a ++ b`. `OpConcat` was a placeholder `I32Add`
(it just summed the two list pointers), so every `++` produced a
garbage/zero-length list. Real implementation, in the canonical
list layout fixed by #255: `[len@+0][elem i @ +4 + i*4]`.
Allocate len(a)+len(b), copy a's then b's elements. *)
let* (ctx1, left_code) = gen_expr ctx left in
let* (ctx2, right_code) = gen_expr ctx1 right in
let (ctx3, heap_idx) = ensure_heap_ptr ctx2 in
let (ctx4, a) = alloc_local ctx3 "__cat_a" in
let (ctx5, b) = alloc_local ctx4 "__cat_b" in
let (ctx6, la) = alloc_local ctx5 "__cat_la" in
let (ctx7, lb) = alloc_local ctx6 "__cat_lb" in
let (ctx8, dst) = alloc_local ctx7 "__cat_dst" in
let (ctx9, k) = alloc_local ctx8 "__cat_k" in
let copy_loop src_ptr count dst_base_off =
(* for k in 0..count: dst[dst_base_off + k] = src[k]
element addr = ptr + idx*4, value/store via static +4 offset
(skips the length word) — exactly the #255 convention. *)
[ I32Const 0l; LocalSet k;
Block (BtEmpty, [ Loop (BtEmpty, [
LocalGet k; LocalGet count; I32GeS; BrIf 1;
(* dst slot: dst + (dst_base_off + k)*4 *)
LocalGet dst;
LocalGet dst_base_off; LocalGet k; I32Add;
I32Const 4l; I32Mul; I32Add;
(* value: src[k] = *(src + k*4 + 4) *)
LocalGet src_ptr; LocalGet k; I32Const 4l; I32Mul; I32Add;
I32Load (2, 4);
I32Store (2, 4);
LocalGet k; I32Const 1l; I32Add; LocalSet k;
Br 0 ]) ]) ]
in
let (ctxA, zero) = alloc_local ctx9 "__cat_zero" in
let code =
left_code @ [LocalSet a] @ right_code @ [LocalSet b] @
[ LocalGet a; I32Load (2, 0); LocalSet la;
LocalGet b; I32Load (2, 0); LocalSet lb;
I32Const 0l; LocalSet zero;
(* dst = heap; heap += 4 + (la+lb)*4 *)
GlobalGet heap_idx; LocalSet dst;
GlobalGet heap_idx;
I32Const 4l;
LocalGet la; LocalGet lb; I32Add; I32Const 4l; I32Mul;
I32Add; I32Add;
GlobalSet heap_idx;
(* dst length = la + lb *)
LocalGet dst; LocalGet la; LocalGet lb; I32Add; I32Store (2, 0) ]
@ copy_loop a la zero (* dst[0..la) := a *)
@ copy_loop b lb la (* dst[la..) := b *)
@ [ LocalGet dst ]
in
Ok (ctxA, code)
| ExprBinary (left, op, right) ->
let* (ctx', left_code) = gen_expr ctx left in
let* (ctx'', right_code) = gen_expr ctx' right in
let op_instr = gen_binop op in
Ok (ctx'', left_code @ right_code @ [op_instr])
| ExprUnary (op, operand) ->
begin match op with
| OpRef | OpMutRef ->
(* Take reference: &expr / &mut expr — same pointer representation;
exclusivity is a static borrow property (CORE-01 pt2 / #177). *)
(* Allocate heap memory, store the value, return pointer *)
let* (ctx', operand_code) = gen_expr ctx operand in
let (ctx_with_heap, alloc_code) = gen_heap_alloc ctx' 4 in
let (ctx_with_ptr, ptr_idx) = alloc_local ctx_with_heap "__ref_ptr" in
let (ctx_with_val, val_idx) = alloc_local ctx_with_ptr "__ref_val" in
(* Strategy: alloc, save ptr, eval operand, save val, store val at ptr, return ptr *)
let ref_code = alloc_code @ [
LocalSet ptr_idx; (* Save allocated pointer *)
] @ operand_code @ [ (* Evaluate operand (value on stack) *)
LocalSet val_idx; (* Save value *)
LocalGet ptr_idx; (* Load pointer *)
LocalGet val_idx; (* Load value *)
I32Store (2, 0); (* Store: mem[ptr+0] = value *)
LocalGet ptr_idx; (* Return pointer *)
] in
Ok (ctx_with_val, ref_code)
| OpDeref ->
(* Dereference: *ptr *)
(* Load value from pointer *)
let* (ctx', ptr_code) = gen_expr ctx operand in
let deref_code = [
I32Load (2, 0); (* Load i32 from pointer *)
] in
Ok (ctx', ptr_code @ deref_code)
| _ ->
(* Other unary ops *)
let* (ctx', operand_code) = gen_expr ctx operand in
let* op_instr = gen_unop op in
let prefix = match op with
| OpNeg -> [I32Const 0l] (* 0 - operand *)
| _ -> []
in
Ok (ctx', prefix @ operand_code @ [op_instr])
end
| ExprIf ei ->
let* (ctx', cond_code) = gen_expr ctx ei.ei_cond in
let* (ctx'', then_code) = gen_expr ctx' ei.ei_then in
let else_result = match ei.ei_else with
| Some e -> gen_expr ctx'' e
| None -> Ok (ctx'', [I32Const 0l]) (* Default to 0 if no else *)
in
let* (ctx_final, else_code) = else_result in
Ok (ctx_final, cond_code @ [If (BtType I32, then_code, else_code)])
| ExprBlock blk ->
gen_block ctx blk
| ExprReturn e_opt ->
begin match e_opt with
| Some e ->
let* (ctx', code) = gen_expr ctx e in
Ok (ctx', code @ [Return])
| None ->
Ok (ctx, [Return])
end
| ExprLet lb ->
let* (ctx', rhs_code) = gen_expr ctx lb.el_value in
let* (ctx'', pat_code) = gen_pattern_bind ctx' lb.el_pat in
begin match lb.el_body with
| Some body ->
let* (ctx_final, body_code) = gen_expr ctx'' body in
Ok (ctx_final, rhs_code @ pat_code @ body_code)
| None ->
Ok (ctx'', rhs_code @ pat_code @ [I32Const 0l])
end
| ExprLambda lam ->
(* Detect free variables (captured from enclosing scope) *)
let param_names = List.map (fun p -> p.p_name.name) lam.elam_params in
let all_free = find_free_vars param_names lam.elam_body in
(* Filter to only variables currently in scope *)
let captured_vars = List.filter (fun name ->
List.mem_assoc name ctx.locals
) (dedup all_free) in
let lambda_id = ctx.next_lambda_id in
(* If there are captured variables, create closure environment *)
let (ctx_after_env, env_code) = if List.length captured_vars > 0 then
(* Create environment tuple with captured values *)
let num_captured = List.length captured_vars in
let env_size = num_captured * 4 in
let (ctx_with_heap, alloc_code) = gen_heap_alloc ctx env_size in
let (ctx_with_temp, env_idx) = alloc_local ctx_with_heap "__closure_env" in
let save_code = [LocalTee env_idx] in
(* Store each captured variable in environment *)
(* Note: Each store consumes env_ptr and value, but we push env_ptr before each store,
so after all stores, one env_ptr remains on stack *)
let store_code = List.mapi (fun i var_name ->
let var_idx = List.assoc var_name ctx.locals in
[
LocalGet env_idx;
LocalGet var_idx;
I32Store (2, i * 4);
]
) captured_vars |> List.concat in
(* Don't push env_idx again - one is already on stack after stores *)
(ctx_with_temp, alloc_code @ save_code @ store_code)
else
(* No captures - environment is null (0) *)
(ctx, [I32Const 0l])
in
(* Create fresh context for lambda function. [next_lambda_id] is
advanced *before* body generation so a nested lambda created
while lowering this body (e.g. a chained CPS continuation, #225
PR3c) gets a distinct id rather than re-using [lambda_id]. *)
let lambda_ctx =
{ ctx_after_env with
locals = []; next_local = 0; loop_depth = 0;
next_lambda_id = lambda_id + 1 } in
(* Environment is always first parameter (even if unused) for uniform calling convention *)
let (ctx_with_env, _) = alloc_local lambda_ctx "__env" in
let env_param_offset = 1 in
(* Regular parameters come after environment *)
let (ctx_with_params, _) = List.fold_left (fun (c, _) param ->
alloc_local c param.p_name.name
) (ctx_with_env, 0) lam.elam_params in
(* Add captured variables to local scope (load from environment) *)
let (ctx_with_captured, load_captured_code) = if List.length captured_vars > 0 then
let (c, code) = List.fold_left (fun (c_acc, code_acc) (i, var_name) ->
let (c', var_idx) = alloc_local c_acc var_name in
let load_code = [
LocalGet 0; (* Environment pointer *)
I32Load (2, i * 4);
LocalSet var_idx;
] in
(c', code_acc @ load_code)
) (ctx_with_params, []) (List.mapi (fun i v -> (i, v)) captured_vars) in
(c, code)
else
(ctx_with_params, [])
in
let param_count = env_param_offset + List.length lam.elam_params in
(* Generate lambda body *)
(* #225 PR3c: if the lambda body is itself an async boundary (a
continuation that chains another async call), transform it so
Thenables compose up the chain; otherwise lower normally. *)
let* (ctx_final, body_code) =
match !async_transform_hook ctx_with_captured lam.elam_body with
| Some r -> r
| None -> gen_expr ctx_with_captured lam.elam_body
in
(* Compute additional locals (beyond parameters and captured vars) *)
let local_count = ctx_final.next_local - param_count in
let locals = if local_count > 0 then
[{ l_count = local_count; l_type = I32 }]
else
[]
in
(* Create function type for lambda (env param always included + regular params) *)
let param_types = I32 :: List.map (fun _ -> I32) lam.elam_params in
let result_type = [I32] in
let func_type = { ft_params = param_types; ft_results = result_type } in
(* Thread the POST-body module-level state forward while keeping the
enclosing scope's local state. The body may have mutated module
accumulators (a nested lambda + its types/globals/datas, #225
PR3c chaining); rebuilding from [ctx_after_env] would silently
drop them. Enclosing locals/next_local/loop_depth/field_layouts
stay from [ctx_after_env] (the lambda's inner locals must not
leak out). For a non-nested body these module fields equal
[ctx_after_env]'s, so behaviour is unchanged. *)
let ctx_post = { ctx_after_env with
types = ctx_final.types;
funcs = ctx_final.funcs;
exports = ctx_final.exports;
imports = ctx_final.imports;
globals = ctx_final.globals;
func_indices = ctx_final.func_indices;
lambda_funcs = ctx_final.lambda_funcs;
next_lambda_id = ctx_final.next_lambda_id;
heap_ptr = ctx_final.heap_ptr;
struct_layouts = ctx_final.struct_layouts;
fn_ret_structs = ctx_final.fn_ret_structs;
variant_tags = ctx_final.variant_tags;
string_data = ctx_final.string_data;
next_string_offset = ctx_final.next_string_offset;
datas = ctx_final.datas;
ownership_annots = ctx_final.ownership_annots;
} in
(* Add type to types list *)
let type_idx = List.length ctx_post.types in
let ctx_with_type = { ctx_post with types = ctx_post.types @ [func_type] } in
(* Create lambda function *)
let lambda_func = {
f_type = type_idx;
f_locals = locals;
f_body = load_captured_code @ body_code;
} in
(* The closure's stored function id MUST be this lambda's index in
the final [lambda_funcs] list, because the element segment maps
table slot i -> the i-th lambda's wasm func (see gen_module), and
wrapHandler dispatches via `table.get(fnId)`. The pre-reserved
[lambda_id] equals the list position ONLY for non-nested lambdas
(id order == append order). A nested lambda (e.g. a chained CPS
continuation, #225 PR3c) is appended *before* its enclosing
lambda yet has a *higher* id, so id ≠ position there. Use the
append position (= current list length) instead. *)
let lambda_slot = List.length ctx_with_type.lambda_funcs in
let ctx_with_lambda = {
ctx_with_type with
lambda_funcs = ctx_with_type.lambda_funcs @ [lambda_func];
} in
(* Return a closure: (function_id, env_pointer) as a 2-element tuple *)
let closure_size = 8 in (* 2 * 4 bytes *)
let (ctx_final2, closure_alloc) = gen_heap_alloc ctx_with_lambda closure_size in
let (ctx_final3, closure_idx) = alloc_local ctx_final2 "__closure" in
(* `LocalSet`, not `LocalTee`: the alloc'd pointer must be consumed
into the local, NOT left on the stack — every subsequent use
re-fetches it via `LocalGet closure_idx`, and the comments below
("Stack is now [closure_idx, env_ptr]") assume an empty stack
here. With `LocalTee` the dangling pointer made `closure_code`
leave TWO values; this never failed before only because the #199
closure path had never been validated by a real wasm engine
end-to-end (static-only until #225 PR2). *)
let closure_code = closure_alloc @ [LocalSet closure_idx] @ [
(* Store function ID at offset 0 *)
LocalGet closure_idx;
I32Const (Int32.of_int lambda_slot);
I32Store (2, 0);
] @ [
(* Store environment pointer at offset 4 *)
LocalGet closure_idx;
] @ env_code @ [
(* env_code left env_ptr on stack, closure_idx is below it *)
(* Stack is now [closure_idx, env_ptr] with env_ptr on top *)
I32Store (2, 4);
(* Return closure pointer *)
LocalGet closure_idx;
] in
Ok (ctx_final3, closure_code)
| ExprApp (func_expr, args) ->
(* Check for built-in WASI functions first *)
begin match func_expr with
| ExprVar id when id.name = "print" && List.length args = 1 ->
(* print(x) - print integer without newline *)
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
(* Allocate temp local to hold the value *)
let (ctx_with_temp, value_temp) = alloc_local ctx_with_arg "__print_value" in
(* Ensure heap pointer is initialized *)
let (ctx_with_heap, heap_idx) = ensure_heap_ptr ctx_with_temp in
(* Get or create fd_write import - assume it's at index 0 for now *)
let fd_write_idx = 0 in
(* Generate WASI print code *)
let print_code = arg_code @ [LocalSet value_temp] @
Wasi_runtime.gen_print_int heap_idx value_temp fd_write_idx in
Ok (ctx_with_heap, print_code)
| ExprVar id when id.name = "println" && List.length args = 0 ->
(* println() - print newline *)
let (ctx_with_temp, temp_local) = alloc_local ctx "__println_temp" in
(* Ensure heap pointer is initialized *)
let (ctx_with_heap, heap_idx) = ensure_heap_ptr ctx_with_temp in
let fd_write_idx = 0 in
let println_code = Wasi_runtime.gen_println heap_idx fd_write_idx temp_local in
Ok (ctx_with_heap, println_code)
| ExprVar id when id.name = "println" && List.length args = 1 ->
(* println(s) - print string and newline *)
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
let (ctx_with_ptr, str_ptr) = alloc_local ctx_with_arg "__println_str_ptr" in
let (ctx_with_temp, temp_local) = alloc_local ctx_with_ptr "__println_temp" in
let (ctx_with_heap, heap_idx) = ensure_heap_ptr ctx_with_temp in
let fd_write_idx = 0 in
let print_code =
arg_code @
[LocalSet str_ptr] @
Wasi_runtime.gen_print_str heap_idx str_ptr fd_write_idx temp_local @
[Drop] @
Wasi_runtime.gen_println heap_idx fd_write_idx temp_local
in
Ok (ctx_with_heap, print_code)
| ExprVar id when id.name = "clock_now_ms" && List.length args = 1 ->
(* ADR-015 S4a (#180): clock_now_ms(clock_id) -> Int ms. Lowers
to a `wasi_snapshot_preview1.clock_time_get` call; the import
is added on-demand at module assembly (S4b refactor), and the
index lives in [ctx.wasi_func_indices] (no hardcoded idx).
Under the S3 component path the reactor adapter bridges this
to wasi:clocks. Effect row is `Time` (tracking-only). *)
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
let (ctx_with_arg2, clock_arg_local) =
alloc_local ctx_with_arg "__clock_id" in
let (ctx_with_scratch, scratch_local) =
alloc_local ctx_with_arg2 "__clock_scratch" in
let (ctx_with_heap, heap_idx) = ensure_heap_ptr ctx_with_scratch in
let clock_func_idx =
try List.assoc "clock_time_get" ctx.wasi_func_indices
with Not_found -> 1 (* defensive; pre-scan guarantees presence *)
in
let code =
arg_code @ [LocalSet clock_arg_local] @
Wasi_runtime.gen_clock_now_ms
heap_idx clock_arg_local scratch_local clock_func_idx
in
Ok (ctx_with_heap, code)
| ExprVar id when id.name = "net_shutdown" && List.length args = 2 ->
(* ADR-015 S6b (#180): net_shutdown(fd, how) -> Int errno.
Lowers to a `wasi_snapshot_preview1.sock_shutdown` import
(on-demand, via the same Effect_sites pre-scan as the S4
builtins). The command adapter bridges to `wasi:sockets/tcp`
at runtime. Pure pass-through: push both args, call. *)
let* (ctx_fd, fd_code) = gen_expr ctx (List.nth args 0) in
let* (ctx_how, how_code) = gen_expr ctx_fd (List.nth args 1) in
let sock_func_idx =
try List.assoc "sock_shutdown" ctx.wasi_func_indices
with Not_found -> 1
in
let code = fd_code @ how_code @ [Call sock_func_idx] in
Ok (ctx_how, code)
| ExprVar id when (id.name = "env_count" || id.name = "arg_count")
&& List.length args = 1 ->
(* ADR-015 S4b (#180): env_count(u: Unit) / arg_count(u: Unit)
— i32 count returns. Lower to the matching
`wasi_snapshot_preview1.{environ,args}_sizes_get` import
(added on-demand at module assembly; idx looked up in
[ctx.wasi_func_indices]). The Unit arg satisfies the
zero-param-fn collapse wart; it is evaluated but its value
is unused. String accessors env_at/arg_at are below. *)
let wasi_name =
if id.name = "env_count" then "environ_sizes_get"
else "args_sizes_get"
in
let sizes_func_idx =
try List.assoc wasi_name ctx.wasi_func_indices
with Not_found -> 1
in
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
let (ctx_with_scratch, scratch_local) =
alloc_local ctx_with_arg ("__" ^ id.name ^ "_scratch") in
let (ctx_with_heap, heap_idx) = ensure_heap_ptr ctx_with_scratch in
let code =
arg_code @ [Drop] @
Wasi_runtime.gen_count_via_sizes_get
heap_idx scratch_local sizes_func_idx
in
Ok (ctx_with_heap, code)
| ExprVar id when id.name = "string_length" && List.length args = 1 ->
(* STDLIB-04e (#332) wasm-backend lowering. AS string layout is
`[len: i32][bytes...]` at the pointer the arg evaluates to —
reading the length is one i32.load at offset 0. The interp
binding (lib/interp.ml) was wired in #362; this handler is
the codegen sibling so tests/codegen/*.affine fixtures that
call string_length (env_at / arg_at / env_count_and_at) can
compile end-to-end. *)
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
Ok (ctx_with_arg, arg_code @ [I32Load (2, 0)])
| ExprVar id when id.name = "string_char_code_at"
&& List.length args = 2 ->
(* PHASE-F string-wall slice 1 (proposals/MIGRATION-PLAN.adoc
§"The two walls"). String indexing on the read-side
`[len: i32 LE][utf8 bytes...]` ABI: byte `i` lives at
`base + 4 + i`. Total function — out-of-bounds (i < 0 OR
i >= len) returns -1, matching the interp oracle
(lib/interp.ml `string_char_code_at`) so the absent-byte
sentinel is shared across both backends. The bounds test must
guard the load: an OOB `I32Load8U` could trap or read foreign
linear memory, so this is `If`, not `Select` (which would
evaluate the load unconditionally). *)
let* (ctx1, s_code) = gen_expr ctx (List.nth args 0) in
let* (ctx2, i_code) = gen_expr ctx1 (List.nth args 1) in
let (ctx3, base_local) = alloc_local ctx2 "__scca_base" in
let (ctx4, idx_local) = alloc_local ctx3 "__scca_idx" in
let code =
s_code @ [LocalSet base_local] @
i_code @ [LocalSet idx_local] @
(* condition: (idx >= 0) && (idx < len), len = i32 at base+0 *)
[ LocalGet idx_local; I32Const 0l; I32GeS ] @
[ LocalGet idx_local; LocalGet base_local; I32Load (2, 0); I32LtS ] @
[ I32And ] @
[ If (BtType I32,
(* in-bounds: zero-extend byte at base + idx + 4 *)
[ LocalGet base_local; LocalGet idx_local; I32Add;
I32Load8U (0, 4) ],
(* out-of-bounds sentinel, shared with the interp oracle *)
[ I32Const (-1l) ]) ]
in
Ok (ctx4, code)
| ExprVar id when id.name = "char_to_int"
&& List.length args = 1 ->
(* PHASE-F string-wall slice 1. `char_to_int` is identity on the
wasm value lattice: a Char is already an i32 byte code (cf. the
`LitChar` lowering to `I32Const (Char.code c)`), and
`string_char_code_at` likewise yields the raw code. Evaluate the
argument and leave its value in place — no conversion
instruction is required. Faithful to the interp oracle
(`Char.code`) for every value the wasm backend can produce. *)
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
Ok (ctx_with_arg, arg_code)
| ExprVar id when id.name = "string_from_char_code"
&& List.length args = 1 ->
(* PHASE-F string-wall slice 2: the write-side of the
`[len: i32 LE][utf8 bytes...]` ABI. `string_from_char_code(n)`
builds a one-byte string from the low 8 bits of `n` — matching
the interp oracle (lib/interp.ml: `String.make 1 (Char.chr
(n land 0xff))`). This is the first heap-allocating string op:
bump-allocate 5 bytes `[len=1][byte]`, store the length word and
the byte, and leave the base pointer as the result.
`I32Store8` writes only the low 8 bits, so it performs the
`land 0xff` masking itself (incl. the correct result for negative
`n`: e.g. -1 stores 0xFF, read back as 255 via the slice-1
`I32Load8U`), so no explicit mask instruction is needed. *)
let* (ctx_n, n_code) = gen_expr ctx (List.hd args) in
let (ctx_a, alloc_code) = gen_heap_alloc ctx_n 5 in
let (ctx_p, ptr_local) = alloc_local ctx_a "__sfcc_ptr" in
let (ctx_v, val_local) = alloc_local ctx_p "__sfcc_val" in
let code =
n_code @ [LocalSet val_local] @ (* val_local = n *)
alloc_code @ [LocalSet ptr_local] @ (* ptr_local = base addr *)
(* [ptr + 0] = length 1 *)
[ LocalGet ptr_local; I32Const 1l; I32Store (2, 0) ] @
(* [ptr + 4] = low byte of n (I32Store8 truncates) *)
[ LocalGet ptr_local; LocalGet val_local; I32Store8 (0, 4) ] @
(* result: the string pointer *)
[ LocalGet ptr_local ]
in
Ok (ctx_v, code)
| ExprVar id when id.name = "string_sub"
&& List.length args = 3 ->
(* PHASE-F string-wall slice 3: the runtime-length copy op.
`string_sub(s, start, length)` returns the substring of `length`
bytes from `start`, with both clamped to the source — matching the
interp oracle (lib/interp.ml):
slen = len s
start' = max 0 (min start slen)
length'= max 0 (min length (slen - start'))
This introduces the two capabilities the rest of the string wall
needs: a *runtime-sized* heap allocation (4 + length' bytes) and a
*byte-copy loop*, both modelled on the list `++` lowering (the
canonical allocate-then-copy idiom in this file). `min`/`max` use
`Select` over the operand pair (no side effects). *)
let* (ctx1, s_code) = gen_expr ctx (List.nth args 0) in
let* (ctx2, start_code) = gen_expr ctx1 (List.nth args 1) in
let* (ctx3, len_code) = gen_expr ctx2 (List.nth args 2) in