From 05b96c10a24aac869a26b80f027ef02dfc3833e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 00:22:11 +0000 Subject: [PATCH] fix(js-codegen): don't re-declare preamble Option/Result constructors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror of the Deno-ESM fix (#606) for the JS backend, which shares the same Some/None/Ok/Err runtime preamble. A program that declares `type Option` / `type Result` (e.g. stdlib/prelude.affine) re-emitted those consts from the TyEnum lowering, redeclaring them (SyntaxError under node). Skip the preamble-provided constructors; user-defined enums are unaffected. The C backend does NOT share this bug — it emits a tag-enum plus distinct constructor functions (TAG_Some / Some()), with no Some/None preamble — so #607's "JS/C" item is JS-only. Adds a JS-path regression guard alongside the existing Deno one. Verified: stdlib/prelude.affine -> JS has a single `const Some` and loads under node; dune test 460 green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8 --- lib/js_codegen.ml | 9 +++++++++ test/test_stdlib_aot.ml | 45 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/js_codegen.ml b/lib/js_codegen.ml index f2f99e28..adb80a67 100644 --- a/lib/js_codegen.ml +++ b/lib/js_codegen.ml @@ -535,7 +535,15 @@ let gen_type_decl ctx (td : type_decl) : unit = and `Type::Variant` references both work. Structs and aliases are erased. *) match td.td_body with | TyEnum variants -> + (* The runtime preamble already declares Some/None/Ok/Err; re-emitting + them for a program that declares `type Option`/`type Result` (e.g. + stdlib/prelude.affine) redeclares the same const (SyntaxError under + node). Skip the preamble-provided constructors. Mirrors the Deno-ESM + fix (#606). *) + let preamble_ctors = [ "Some"; "None"; "Ok"; "Err" ] in List.iter (fun (vd : variant_decl) -> + if List.mem vd.vd_name.name preamble_ctors then () + else begin let name = mangle vd.vd_name.name in let arity = List.length vd.vd_fields in if arity = 0 then @@ -550,6 +558,7 @@ let gen_type_decl ctx (td : type_decl) : unit = (Printf.sprintf "const %s = (%s) => ({ tag: \"%s\", values: [%s] });" name (String.concat ", " params) vd.vd_name.name (String.concat ", " params)) + end ) variants; emit ctx "\n" | TyStruct _ | TyAlias _ -> diff --git a/test/test_stdlib_aot.ml b/test/test_stdlib_aot.ml index 60d36f17..79af90e8 100644 --- a/test/test_stdlib_aot.ml +++ b/test/test_stdlib_aot.ml @@ -61,6 +61,31 @@ let pipeline_to_deno (prog : Ast.program) : (string, string) result = | Error e -> Error (Printf.sprintf "deno-codegen: %s" e) | Ok js -> Ok js))) +(** Same pipeline, JS (non-ESM) backend. Shares the Some/None/Ok/Err runtime + preamble with Deno-ESM, so it has the same duplicate-constructor surface. *) +let pipeline_to_js (prog : Ast.program) : (string, string) result = + let ld = loader () in + match Resolve.resolve_program_with_loader prog ld with + | Error (e, sp) -> + Error (Printf.sprintf "resolve: %s @ %s" + (Resolve.show_resolve_error e) (Span.show sp)) + | Ok (rctx, itc) -> + (match + Typecheck.check_program + ~import_types:itc.Typecheck.name_types rctx.symbols prog + with + | Error e -> + Error (Printf.sprintf "typecheck: %s" (Typecheck.format_type_error e)) + | Ok _ -> + (match Borrow.check_program rctx.symbols prog with + | Error e -> + Error (Printf.sprintf "borrow: %s" (Borrow.format_borrow_error e)) + | Ok () -> + let flat = Module_loader.flatten_imports ld prog in + (match Js_codegen.codegen_js flat rctx.symbols with + | Error e -> Error (Printf.sprintf "js-codegen: %s" e) + | Ok js -> Ok js))) + (** Full AOT pipeline to the core-Wasm backend: resolve -> typecheck -> borrow -> [Codegen.generate_module] (loader-aware). Mirrors [pipeline_to_deno] but targets the backend whose cross-module constructor @@ -262,12 +287,26 @@ let test_deno_no_duplicate_option_ctor () = "`const Some` declared exactly once (preamble only, not re-emitted)" 1 (count_substr "const Some" js)) -let deno_dup_ctor_tests = +let test_js_no_duplicate_option_ctor () = + match Parse_driver.parse_string ~file:"" local_option_src with + | exception e -> + Alcotest.failf "local-option parse raised: %s" (Printexc.to_string e) + | prog -> + (match pipeline_to_js prog with + | Error m -> Alcotest.failf "js codegen failed: %s" m + | Ok js -> + Alcotest.(check int) + "`const Some` declared exactly once (preamble only, not re-emitted)" + 1 (count_substr "const Some" js)) + +let dup_ctor_tests = [ Alcotest.test_case "declared Option does not duplicate preamble ctor (Deno)" - `Quick test_deno_no_duplicate_option_ctor ] + `Quick test_deno_no_duplicate_option_ctor; + Alcotest.test_case "declared Option does not duplicate preamble ctor (JS)" + `Quick test_js_no_duplicate_option_ctor ] let tests = [ ("STAGE-A AOT smoke (#136)", aot_smoke_tests); ("STAGE-A multi-module integration (#137)", integration_tests); ("cross-module constructor linking, Wasm (#138)", xmod_constructor_tests); - ("Deno-ESM no duplicate Option/Result constructor", deno_dup_ctor_tests) ] + ("Deno-ESM / JS no duplicate Option/Result constructor", dup_ctor_tests) ]