From 17fcec65cca5cda3ccd9a8ec054c14bdb144f349 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 26 May 2026 17:33:48 +0100 Subject: [PATCH] fix(codegen): register struct_layouts for record-type aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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) --- lib/codegen.ml | 9 +++++++++ test/test_e2e.ml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/lib/codegen.ml b/lib/codegen.ml index ee6cf8c6..57910041 100644 --- a/lib/codegen.ml +++ b/lib/codegen.ml @@ -2452,6 +2452,15 @@ let gen_decl (ctx : context) (decl : top_level) : context result = ExprRecord store path which writes fields in declaration order. *) let layout = List.mapi (fun i sf -> (sf.sf_name.name, i * 4)) fields in Ok { ctx with struct_layouts = (td.td_name.name, layout) :: ctx.struct_layouts } + | TyAlias (TyRecord (rfs, _)) -> + (* `type X = { a: T, b: U, ... }` is parsed as an alias to TyRecord. + Without this branch, the alias falls through to the catch-all + below and never registers a layout, causing every parameter / + return of type X to read all fields at offset 0 (silent + miscompile, not a crash). Mirrors the TyStruct branch above + but unpacks the alias. *) + let layout = List.mapi (fun i rf -> (rf.rf_name.name, i * 4)) rfs in + Ok { ctx with struct_layouts = (td.td_name.name, layout) :: ctx.struct_layouts } | TyAlias _ -> Ok ctx end diff --git a/test/test_e2e.ml b/test/test_e2e.ml index db1ebf72..a65f2c01 100644 --- a/test/test_e2e.ml +++ b/test/test_e2e.ml @@ -644,6 +644,46 @@ let test_wasm_lambda () = | Error msg -> Alcotest.fail msg | Ok _wasm_mod -> () +(* ---------------------------------------------------------------------------- + Regression: `type X = { ... }` (a TyAlias wrapping a TyRecord) must + register a struct_layouts entry just like `struct X { ... }` (TyStruct). + Without that, every parameter / return of type X reads all fields at + offset 0 — a silent miscompile, not a crash. See lib/codegen.ml + `gen_decl` TopType branch. *) + +let codegen_decl_for src = + let prog = Parse_driver.parse_string ~file:"" src in + match prog.prog_decls with + | decl :: _ -> decl + | [] -> Alcotest.fail "expected at least one top-level decl" + +let test_codegen_record_alias_registers_struct_layout () = + let decl = codegen_decl_for "type State = { health: Int, score: Int };" in + match Codegen.gen_decl (Codegen.create_context ()) decl with + | Error e -> + Alcotest.fail (Printf.sprintf "gen_decl errored: %s" + (Codegen.show_codegen_error e)) + | Ok ctx -> + let layout = List.assoc_opt "State" ctx.struct_layouts in + Alcotest.(check (option (list (pair string int)))) + "State alias registers field layout" + (Some [("health", 0); ("score", 4)]) + layout + +let test_codegen_plain_alias_does_not_register_layout () = + (* Sanity: the new pattern must not over-broaden — `type X = Int` + should still hit the catch-all and leave struct_layouts empty. *) + let decl = codegen_decl_for "type Plain = Int;" in + match Codegen.gen_decl (Codegen.create_context ()) decl with + | Error e -> + Alcotest.fail (Printf.sprintf "gen_decl errored: %s" + (Codegen.show_codegen_error e)) + | Ok ctx -> + Alcotest.(check (option (list (pair string int)))) + "non-record alias registers no layout" + None + (List.assoc_opt "Plain" ctx.struct_layouts) + let wasm_tests = [ Alcotest.test_case "bitwise codegen" `Quick test_wasm_bitwise; Alcotest.test_case "arithmetic codegen" `Quick test_wasm_arithmetic; @@ -651,6 +691,10 @@ let wasm_tests = [ Alcotest.test_case "write binary" `Quick test_wasm_write_binary; Alcotest.test_case "full pipeline" `Quick test_wasm_full_pipeline; Alcotest.test_case "lambda codegen" `Quick test_wasm_lambda; + Alcotest.test_case "record-alias registers struct_layouts" `Quick + test_codegen_record_alias_registers_struct_layout; + Alcotest.test_case "non-record alias leaves struct_layouts alone" `Quick + test_codegen_plain_alias_does_not_register_layout; ] (* ============================================================================