From 000ee8061657a5a9e6b5aa3b381cbeffd872e5cc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 18:41:47 +0000 Subject: [PATCH] fix(codegen-deno): don't re-declare preamble Option/Result constructors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Deno-ESM runtime preamble already declares Some/None/Ok/Err. gen_type_decl re-emitted them for any program that DECLARES `type Option`/`type Result` (e.g. stdlib/prelude.affine), so the emitted module crashed under node with `SyntaxError: Identifier 'Some' has already been declared`. The #136 AOT smoke never caught it — it only checks the emitted module is non-empty, never runs it. Skip the variants the preamble already provides (Some/None/Ok/Err) when lowering a TyEnum; user-defined enums are unaffected. Add a regression test asserting `const Some` is declared exactly once. This is the locally-declared sibling of the imported-constructor duplication fixed in #604; together they close the duplicate-constructor class on the Deno-ESM backend. Verified: stdlib/prelude.affine now runs under node; dune test 459 green; tools/run_codegen_deno_tests.sh all harnesses pass under node. The JS and C backends share the same latent preamble/declaration duplication (not executed in CI); tracked separately. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8 --- lib/codegen_deno.ml | 12 +++++++++++ test/test_stdlib_aot.ml | 44 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index 4e7bf9ea..89b2bf1e 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -1685,7 +1685,18 @@ let gen_type_decl ctx (td : type_decl) : unit = match td.td_body with | TyEnum variants -> let exp = if visibility_is_public td.td_vis then "export " else "" in + (* The runtime preamble (see [prelude]) already declares the foundational + Option/Result constructors Some/None/Ok/Err. Re-emitting them here for + a program that *declares* `type Option`/`type Result` (e.g. + stdlib/prelude.affine) redeclared the same `const` and crashed the + emitted module under node with `SyntaxError: Identifier 'Some' has + already been declared`. Skip any variant the preamble already provides. + (The #136 AOT smoke never caught this — it only checks the output is + non-empty, never runs it.) *) + 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 @@ -1703,6 +1714,7 @@ let gen_type_decl ctx (td : type_decl) : unit = "%sconst %s = (%s) => ({ tag: \"%s\", values: [%s] });" exp name (String.concat ", " ps) vd.vd_name.name (String.concat ", " ps)) + end ) variants; emit ctx "\n" | TyStruct _ | TyAlias _ | TyExtern -> diff --git a/test/test_stdlib_aot.ml b/test/test_stdlib_aot.ml index 7f94db1e..60d36f17 100644 --- a/test/test_stdlib_aot.ml +++ b/test/test_stdlib_aot.ml @@ -225,7 +225,49 @@ let xmod_constructor_tests = [ Alcotest.test_case "imported Option/Result constructors -> Wasm" `Quick test_imported_constructors_wasm ] +(* ---- Deno-ESM: no duplicate Option/Result constructor declaration -------- + + The Deno-ESM runtime preamble already declares Some/None/Ok/Err. A module + that *declares* `type Option`/`type Result` (e.g. stdlib/prelude.affine) + must not re-emit those consts, or the emitted module crashes under node + with `SyntaxError: Identifier 'Some' has already been declared`. The #136 + AOT smoke never caught this (it only checks the output is non-empty, never + runs it), so this asserts the foundational constructor is declared exactly + once. Counterpart guard for the codegen-deno run-under-node CI step. *) +let local_option_src = {| +module localopt; +pub type Option = Some(T) | None +pub fn wrap(x: Int) -> Option { Some(x) } +pub fn empty() -> Option { None } +|} + +let count_substr (needle : string) (hay : string) : int = + let re = Str.regexp_string needle in + let rec loop pos acc = + match Str.search_forward re hay pos with + | exception Not_found -> acc + | i -> loop (i + String.length needle) (acc + 1) + in + loop 0 0 + +let test_deno_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_deno prog with + | Error m -> Alcotest.failf "deno 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 deno_dup_ctor_tests = + [ Alcotest.test_case "declared Option does not duplicate preamble ctor (Deno)" + `Quick test_deno_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) ] + ("cross-module constructor linking, Wasm (#138)", xmod_constructor_tests); + ("Deno-ESM no duplicate Option/Result constructor", deno_dup_ctor_tests) ]