@@ -32,7 +32,15 @@ type context = {
3232 lambda_funcs : func list ; (* * lifted lambda functions *)
3333 next_lambda_id : int ; (* * next lambda function ID *)
3434 heap_ptr : int option ; (* * global index for heap pointer, if initialized *)
35- field_layouts : (string * (string * int ) list ) list ; (* * type name -> [(field, offset)] *)
35+ field_layouts : (string * (string * int ) list ) list ; (* * variable name -> [(field, offset)] *)
36+ struct_layouts : (string * (string * int ) list ) list ;
37+ (* * Struct type name -> [(field, offset)]. Registered from TopType(TyStruct)
38+ at decl time so function-parameter and call-result field accesses can
39+ recover the field layout by type, not by let-binding shape. *)
40+ fn_ret_structs : (string * string ) list ;
41+ (* * Function name -> struct type name it returns (if any). Lets a
42+ `let s = make()` call site register s's field layout in field_layouts
43+ when the callee's return type is a known struct. *)
3644 variant_tags : (string * int ) list ; (* * constructor name -> tag (int) *)
3745 string_data : (string * int ) list ; (* * string content -> memory offset *)
3846 next_string_offset : int ; (* * next available offset for string data *)
@@ -78,6 +86,8 @@ let create_context () : context = {
7886 next_lambda_id = 0 ;
7987 heap_ptr = None ;
8088 field_layouts = [] ;
89+ struct_layouts = [] ;
90+ fn_ret_structs = [] ;
8191 variant_tags = [] ;
8292 string_data = [] ;
8393 next_string_offset = 2048 ; (* Start strings after heap at offset 2048 *)
@@ -99,6 +109,17 @@ let ownership_kind_of_param (p : param) : ownership_kind =
99109 | TyMut _ -> ExclBorrow
100110 | _ -> Unrestricted
101111
112+ (* * If [ty] names a known struct (through any number of own/ref/mut wrappers),
113+ return that struct's name. Lets us recover a struct's field layout from
114+ parameter and return-type annotations so `.field_N` reads use the correct
115+ offset instead of defaulting to 0. *)
116+ let rec struct_name_of_ty (ty : type_expr ) : string option =
117+ match ty with
118+ | TyCon id -> Some id.name
119+ | TyApp (id , _ ) -> Some id.name
120+ | TyOwn inner | TyRef inner | TyMut inner -> struct_name_of_ty inner
121+ | _ -> None
122+
102123(* * Extract ownership kind from an optional return type expression *)
103124let ownership_kind_of_ret (ret : type_expr option ) : ownership_kind =
104125 match ret with
@@ -397,8 +418,20 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
397418 Ok (ctx', [instr])
398419
399420 | ExprVar id ->
400- let * idx = lookup_local ctx id.name in
401- Ok (ctx, [LocalGet idx])
421+ begin match lookup_local ctx id.name with
422+ | Ok idx -> Ok (ctx, [LocalGet idx])
423+ | Error _ ->
424+ (* Fallback: bare identifier that names a zero-arity enum variant.
425+ Matches the ExprCall resolution at line 658 so that both
426+ `Initialised` and `Initialised()` work as expressions when the
427+ name is known as a variant constructor. Without this, a match
428+ arm body of the form `Uninitialised => Initialised` fails with
429+ UnboundVariable even though the parser accepts it. *)
430+ begin match List. assoc_opt id.name ctx.variant_tags with
431+ | Some tag -> Ok (ctx, [I32Const (Int32. of_int tag)])
432+ | None -> Error (UnboundVariable id.name)
433+ end
434+ end
402435
403436 | ExprBinary (left , op , right ) ->
404437 let * (ctx', left_code) = gen_expr ctx left in
@@ -1215,20 +1248,24 @@ and gen_pattern (ctx : context) (scrutinee_local : int) (pat : pattern)
12151248 ({ ctx with variant_tags = (con.name, new_tag) :: ctx.variant_tags }, new_tag)
12161249 in
12171250
1218- (* Allocate temp for match result *)
1219- let (ctx_with_temp, match_result_idx) = alloc_local ctx_with_tag " __match_result" in
1220-
1221- (* Test: load tag from scrutinee and compare, save result *)
1251+ (* Test: load tag from scrutinee and compare. Leaves the boolean
1252+ on the stack. Binding code below is stack-neutral (see net-zero
1253+ analysis in bind_fields), so the boolean survives through to the
1254+ end of full_code. Prior implementation saved the bool via
1255+ LocalTee and re-pushed via LocalGet at the end, which left TWO
1256+ booleans on the stack and broke WASM validation in any match arm
1257+ whose body produced an i32 result — e.g. enum-in-match returning
1258+ distinct zero-arity or arg constructors across arms. *)
12221259 let tag_test = [
12231260 LocalGet scrutinee_local; (* variant pointer *)
12241261 I32Load (2 , 0 ); (* load tag from offset 0 *)
12251262 I32Const (Int32. of_int tag);
1226- I32Eq ;
1227- LocalTee match_result_idx; (* Save match result *)
1263+ I32Eq ; (* bool on stack — one value *)
12281264 ] in
12291265
1230- (* Extract fields and bind variables *)
1231- (* For now, only support PatVar sub-patterns *)
1266+ (* Extract fields and bind variables. Each bind is net-zero on the
1267+ stack (LocalGet +1, I32Load 0, LocalSet -1), so the tag-test
1268+ boolean above remains on top of the stack as the pattern result. *)
12321269 let rec bind_fields ctx_acc bindings_acc offset patterns =
12331270 match patterns with
12341271 | [] -> Ok (ctx_acc, bindings_acc)
@@ -1252,10 +1289,9 @@ and gen_pattern (ctx : context) (scrutinee_local : int) (pat : pattern)
12521289 end
12531290 in
12541291
1255- let * (ctx_final, binding_code) = bind_fields ctx_with_temp [] 4 sub_patterns in
1292+ let * (ctx_final, binding_code) = bind_fields ctx_with_tag [] 4 sub_patterns in
12561293
1257- (* Combine: test tag, bind fields, return test result *)
1258- let full_code = tag_test @ binding_code @ [LocalGet match_result_idx] in
1294+ let full_code = tag_test @ binding_code in
12591295
12601296 Ok (ctx_final, full_code, [] )
12611297
@@ -1378,15 +1414,46 @@ and gen_stmt (ctx : context) (stmt : stmt) : (context * instr list) result =
13781414 let * (ctx', rhs_code) = gen_expr ctx sl.sl_value in
13791415 begin match sl.sl_pat with
13801416 | PatVar id ->
1381- let (ctx'', idx) = alloc_local ctx' id.name in
1382- (* If RHS is a record, track its field layout *)
1383- let ctx_with_layout = match sl.sl_value with
1417+ (* Track the bound variable's field layout so subsequent `.field_N`
1418+ reads pick the right offset. Three sources, in order:
1419+ 1. Explicit `let s: State = ...` annotation → look up struct_layouts.
1420+ 2. RHS is a record literal → layout from literal's field order.
1421+ 3. RHS is `f(...)` where f's declared return type is a struct
1422+ → look up fn_ret_structs then struct_layouts.
1423+ 4. RHS is another bound variable `let t = s` where s has a
1424+ tracked layout → copy it.
1425+ Any source misses fall back to no tracking (existing behaviour). *)
1426+ let layout_from_ty_annot () =
1427+ match sl.sl_ty with
1428+ | Some ty ->
1429+ begin match struct_name_of_ty ty with
1430+ | Some sname -> List. assoc_opt sname ctx'.struct_layouts
1431+ | None -> None
1432+ end
1433+ | None -> None
1434+ in
1435+ let layout_from_rhs () =
1436+ match sl.sl_value with
13841437 | ExprRecord rec_expr ->
1385- let field_layout = List. mapi (fun i (field_name , _ ) ->
1386- (field_name.name, i * 4 )
1387- ) rec_expr.er_fields in
1388- { ctx'' with field_layouts = (id.name, field_layout) :: ctx''.field_layouts }
1389- | _ -> ctx''
1438+ Some (List. mapi (fun i (fn , _ ) -> (fn.name, i * 4 )) rec_expr.er_fields)
1439+ | ExprApp (ExprVar fn_id , _ ) ->
1440+ begin match List. assoc_opt fn_id.name ctx'.fn_ret_structs with
1441+ | Some sname -> List. assoc_opt sname ctx'.struct_layouts
1442+ | None -> None
1443+ end
1444+ | ExprVar src_id -> List. assoc_opt src_id.name ctx'.field_layouts
1445+ | _ -> None
1446+ in
1447+ let layout_opt =
1448+ match layout_from_ty_annot () with
1449+ | Some _ as s -> s
1450+ | None -> layout_from_rhs ()
1451+ in
1452+ let (ctx'', idx) = alloc_local ctx' id.name in
1453+ let ctx_with_layout = match layout_opt with
1454+ | Some layout ->
1455+ { ctx'' with field_layouts = (id.name, layout) :: ctx''.field_layouts }
1456+ | None -> ctx''
13901457 in
13911458 Ok (ctx_with_layout, rhs_code @ [LocalSet idx])
13921459 | _ ->
@@ -1659,9 +1726,23 @@ let gen_function (ctx : context) (fd : fn_decl) : (context * func) result =
16591726 (* Create fresh context for function scope, but preserve lambda_funcs and next_lambda_id *)
16601727 let fn_ctx = { ctx with locals = [] ; next_local = 0 ; loop_depth = 0 } in
16611728
1662- (* Parameters become locals 0..n-1 *)
1729+ (* Parameters become locals 0..n-1. When a parameter's declared type
1730+ names a known struct, register its field layout under the parameter
1731+ name so body-side `.field_N` reads resolve to the correct offset
1732+ rather than defaulting to 0. *)
16631733 let (ctx_with_params, _) = List. fold_left (fun (c , _ ) param ->
1664- alloc_local c param.p_name.name
1734+ let (c', idx) = alloc_local c param.p_name.name in
1735+ let c'' =
1736+ match struct_name_of_ty param.p_ty with
1737+ | Some sname ->
1738+ begin match List. assoc_opt sname c'.struct_layouts with
1739+ | Some layout ->
1740+ { c' with field_layouts = (param.p_name.name, layout) :: c'.field_layouts }
1741+ | None -> c'
1742+ end
1743+ | None -> c'
1744+ in
1745+ (c'', idx)
16651746 ) (fn_ctx, 0 ) fd.fd_params in
16661747
16671748 let param_count = List. length fd.fd_params in
@@ -1711,10 +1792,21 @@ let gen_decl (ctx : context) (decl : top_level) : context result =
17111792 let param_kinds = List. map ownership_kind_of_param fd.fd_params in
17121793 let ret_kind = ownership_kind_of_ret fd.fd_ret_ty in
17131794
1714- (* Register function name to index mapping and record ownership annotations *)
1795+ (* Register function name to index mapping and record ownership annotations.
1796+ Also record the function's return struct name (if any) so call sites
1797+ `let s = f(...)` can register s's field layout. *)
1798+ let fn_ret_structs' = match fd.fd_ret_ty with
1799+ | Some ty ->
1800+ begin match struct_name_of_ty ty with
1801+ | Some sname -> (fd.fd_name.name, sname) :: ctx_with_type.fn_ret_structs
1802+ | None -> ctx_with_type.fn_ret_structs
1803+ end
1804+ | None -> ctx_with_type.fn_ret_structs
1805+ in
17151806 let ctx_with_func_idx = { ctx_with_type with
17161807 func_indices = ctx_with_type.func_indices @ [(fd.fd_name.name, func_idx)];
17171808 ownership_annots = ctx_with_type.ownership_annots @ [(func_idx, param_kinds, ret_kind)];
1809+ fn_ret_structs = fn_ret_structs';
17181810 } in
17191811
17201812 (* Generate function with correct type index *)
@@ -1756,7 +1848,6 @@ let gen_decl (ctx : context) (decl : top_level) : context result =
17561848 Ok ctx''
17571849
17581850 | TopType td ->
1759- (* Register variant tags for enum types *)
17601851 begin match td.td_body with
17611852 | TyEnum variants ->
17621853 (* Assign sequential tags to each variant constructor *)
@@ -1765,8 +1856,14 @@ let gen_decl (ctx : context) (decl : top_level) : context result =
17651856 { c_acc with variant_tags = (vd.vd_name.name, idx) :: c_acc.variant_tags }
17661857 ) ctx (List. mapi (fun i v -> (i, v)) variants) in
17671858 Ok ctx_with_tags
1768- | _ ->
1769- (* Other type declarations (alias, struct) don't need codegen *)
1859+ | TyStruct fields ->
1860+ (* Build the struct's field layout so function parameters and call
1861+ results of this type can resolve `.field_N` to the correct offset.
1862+ Layout: fields packed sequentially at 4-byte offsets, matching the
1863+ ExprRecord store path which writes fields in declaration order. *)
1864+ let layout = List. mapi (fun i sf -> (sf.sf_name.name, i * 4 )) fields in
1865+ Ok { ctx with struct_layouts = (td.td_name.name, layout) :: ctx.struct_layouts }
1866+ | TyAlias _ ->
17701867 Ok ctx
17711868 end
17721869
0 commit comments