Skip to content

Commit 839c272

Browse files
fix(codegen): register struct_layouts for record-type aliases (#388)
## Summary Record-type aliases (`type X = { ... }`) silently miscompiled when used as function parameters or return types: every field access on such a value resolved to offset 0. ### Root cause `type X = { a: T, b: U }` parses to `TopType { td_body = TyAlias (TyRecord (rfs, _)) }` (parser.mly:431). The TopType branch in `gen_decl` (codegen.ml:2439) handled the analogous `TyStruct` case by registering a 4-byte-stride layout in `ctx.struct_layouts`, but the TyAlias branch caught everything with `TyAlias _ -> Ok ctx`. With no layout registered, every `.field_N` access fell back to offset 0 — a silent runtime miscompile, not a validation error or compile failure. The fix adds a `TyAlias (TyRecord (rfs, _))` branch immediately before the catch-all, mirroring the TyStruct case but unpacking the alias. ## Regression coverage Two new tests in the `E2E WASM` group: - **`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`. Verified to fail on main without the fix (stash-revert reproduces `expected Some [...] but got None`); passes with it. - **`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. ## Verification \`\`\` $ dune build && dune runtest … Test Successful in 0.082s. 329 tests run. \`\`\` Was 327, now 329 with the two new tests. Full suite green. ## Discovered during Field trial taking `airborne-submarine-squadron` (a 29-field state record stepped at 60 fps over WASM) from \"compiles\" to \"runs to conclusion\". The product currently ships via the linear backend with flat-record boundaries; this fix removes the flatness constraint at the per-record-alias level. WasmGC-backend record/tuple layout has a separate, independent defect — out of scope here. ## Test plan - [x] `dune build` clean - [x] `dune runtest` → 329/329 green - [x] New regression test verified to fail on main without the fix - [x] Catch-all `TyAlias _` still reached for non-record aliases - [ ] CI green 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2395c76 commit 839c272

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)