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) ]