Skip to content

Commit 17fcec6

Browse files
hyperpolymathclaude
andcommitted
fix(codegen): register struct_layouts for record-type aliases
`type X = { a: T, b: U }` parses as `TopType { td_body = TyAlias (TyRecord (rfs, _)) }`. The TopType branch in `gen_decl` handled TyStruct (registering a 4-byte-stride field layout in `ctx.struct_layouts`) but the TyAlias branch caught everything with `TyAlias _ -> Ok ctx`, so record aliases never registered. The downstream symptom is silent: every parameter or return of such an alias resolves `.field_N` to offset 0 because the layout lookup returns None. The miscompile only surfaces at runtime via wrong field values, not a Wasm validation error or a compile failure. Fix mirrors the existing TyStruct branch but unpacks the alias. Regression coverage: - `record-alias registers struct_layouts` — parses `type State = { health: Int, score: Int };`, calls `gen_decl` directly, asserts `("State", [("health", 0); ("score", 4)])` appears in `ctx.struct_layouts`. This test fails on main without the fix (verified by stash-revert) with `expected Some [...] but got None`. - `non-record alias leaves struct_layouts alone` — guards against accidental over-broadening: `type Plain = Int` must still hit the catch-all and add no layout entry. 327 → 329 tests; full suite green. Refs affinescript ASBSQ trial 2026-05-19 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d1f0e40 commit 17fcec6

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

lib/codegen.ml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,6 +2452,15 @@ let gen_decl (ctx : context) (decl : top_level) : context result =
24522452
ExprRecord store path which writes fields in declaration order. *)
24532453
let layout = List.mapi (fun i sf -> (sf.sf_name.name, i * 4)) fields in
24542454
Ok { ctx with struct_layouts = (td.td_name.name, layout) :: ctx.struct_layouts }
2455+
| TyAlias (TyRecord (rfs, _)) ->
2456+
(* `type X = { a: T, b: U, ... }` is parsed as an alias to TyRecord.
2457+
Without this branch, the alias falls through to the catch-all
2458+
below and never registers a layout, causing every parameter /
2459+
return of type X to read all fields at offset 0 (silent
2460+
miscompile, not a crash). Mirrors the TyStruct branch above
2461+
but unpacks the alias. *)
2462+
let layout = List.mapi (fun i rf -> (rf.rf_name.name, i * 4)) rfs in
2463+
Ok { ctx with struct_layouts = (td.td_name.name, layout) :: ctx.struct_layouts }
24552464
| TyAlias _ ->
24562465
Ok ctx
24572466
end

test/test_e2e.ml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,13 +644,57 @@ let test_wasm_lambda () =
644644
| Error msg -> Alcotest.fail msg
645645
| Ok _wasm_mod -> ()
646646

647+
(* ----------------------------------------------------------------------------
648+
Regression: `type X = { ... }` (a TyAlias wrapping a TyRecord) must
649+
register a struct_layouts entry just like `struct X { ... }` (TyStruct).
650+
Without that, every parameter / return of type X reads all fields at
651+
offset 0 — a silent miscompile, not a crash. See lib/codegen.ml
652+
`gen_decl` TopType branch. *)
653+
654+
let codegen_decl_for src =
655+
let prog = Parse_driver.parse_string ~file:"<regression>" src in
656+
match prog.prog_decls with
657+
| decl :: _ -> decl
658+
| [] -> Alcotest.fail "expected at least one top-level decl"
659+
660+
let test_codegen_record_alias_registers_struct_layout () =
661+
let decl = codegen_decl_for "type State = { health: Int, score: Int };" in
662+
match Codegen.gen_decl (Codegen.create_context ()) decl with
663+
| Error e ->
664+
Alcotest.fail (Printf.sprintf "gen_decl errored: %s"
665+
(Codegen.show_codegen_error e))
666+
| Ok ctx ->
667+
let layout = List.assoc_opt "State" ctx.struct_layouts in
668+
Alcotest.(check (option (list (pair string int))))
669+
"State alias registers field layout"
670+
(Some [("health", 0); ("score", 4)])
671+
layout
672+
673+
let test_codegen_plain_alias_does_not_register_layout () =
674+
(* Sanity: the new pattern must not over-broaden — `type X = Int`
675+
should still hit the catch-all and leave struct_layouts empty. *)
676+
let decl = codegen_decl_for "type Plain = Int;" in
677+
match Codegen.gen_decl (Codegen.create_context ()) decl with
678+
| Error e ->
679+
Alcotest.fail (Printf.sprintf "gen_decl errored: %s"
680+
(Codegen.show_codegen_error e))
681+
| Ok ctx ->
682+
Alcotest.(check (option (list (pair string int))))
683+
"non-record alias registers no layout"
684+
None
685+
(List.assoc_opt "Plain" ctx.struct_layouts)
686+
647687
let wasm_tests = [
648688
Alcotest.test_case "bitwise codegen" `Quick test_wasm_bitwise;
649689
Alcotest.test_case "arithmetic codegen" `Quick test_wasm_arithmetic;
650690
Alcotest.test_case "simple program" `Quick test_wasm_simple;
651691
Alcotest.test_case "write binary" `Quick test_wasm_write_binary;
652692
Alcotest.test_case "full pipeline" `Quick test_wasm_full_pipeline;
653693
Alcotest.test_case "lambda codegen" `Quick test_wasm_lambda;
694+
Alcotest.test_case "record-alias registers struct_layouts" `Quick
695+
test_codegen_record_alias_registers_struct_layout;
696+
Alcotest.test_case "non-record alias leaves struct_layouts alone" `Quick
697+
test_codegen_plain_alias_does_not_register_layout;
654698
]
655699

656700
(* ============================================================================

0 commit comments

Comments
 (0)