Skip to content

Commit 000ee80

Browse files
committed
fix(codegen-deno): don't re-declare preamble Option/Result constructors
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8
1 parent c7922cf commit 000ee80

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

lib/codegen_deno.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,7 +1685,18 @@ let gen_type_decl ctx (td : type_decl) : unit =
16851685
match td.td_body with
16861686
| TyEnum variants ->
16871687
let exp = if visibility_is_public td.td_vis then "export " else "" in
1688+
(* The runtime preamble (see [prelude]) already declares the foundational
1689+
Option/Result constructors Some/None/Ok/Err. Re-emitting them here for
1690+
a program that *declares* `type Option`/`type Result` (e.g.
1691+
stdlib/prelude.affine) redeclared the same `const` and crashed the
1692+
emitted module under node with `SyntaxError: Identifier 'Some' has
1693+
already been declared`. Skip any variant the preamble already provides.
1694+
(The #136 AOT smoke never caught this — it only checks the output is
1695+
non-empty, never runs it.) *)
1696+
let preamble_ctors = [ "Some"; "None"; "Ok"; "Err" ] in
16881697
List.iter (fun (vd : variant_decl) ->
1698+
if List.mem vd.vd_name.name preamble_ctors then ()
1699+
else begin
16891700
let name = mangle vd.vd_name.name in
16901701
let arity = List.length vd.vd_fields in
16911702
if arity = 0 then
@@ -1703,6 +1714,7 @@ let gen_type_decl ctx (td : type_decl) : unit =
17031714
"%sconst %s = (%s) => ({ tag: \"%s\", values: [%s] });"
17041715
exp name (String.concat ", " ps) vd.vd_name.name
17051716
(String.concat ", " ps))
1717+
end
17061718
) variants;
17071719
emit ctx "\n"
17081720
| TyStruct _ | TyAlias _ | TyExtern ->

test/test_stdlib_aot.ml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,49 @@ let xmod_constructor_tests =
225225
[ Alcotest.test_case "imported Option/Result constructors -> Wasm" `Quick
226226
test_imported_constructors_wasm ]
227227

228+
(* ---- Deno-ESM: no duplicate Option/Result constructor declaration --------
229+
230+
The Deno-ESM runtime preamble already declares Some/None/Ok/Err. A module
231+
that *declares* `type Option`/`type Result` (e.g. stdlib/prelude.affine)
232+
must not re-emit those consts, or the emitted module crashes under node
233+
with `SyntaxError: Identifier 'Some' has already been declared`. The #136
234+
AOT smoke never caught this (it only checks the output is non-empty, never
235+
runs it), so this asserts the foundational constructor is declared exactly
236+
once. Counterpart guard for the codegen-deno run-under-node CI step. *)
237+
let local_option_src = {|
238+
module localopt;
239+
pub type Option<T> = Some(T) | None
240+
pub fn wrap(x: Int) -> Option<Int> { Some(x) }
241+
pub fn empty() -> Option<Int> { None }
242+
|}
243+
244+
let count_substr (needle : string) (hay : string) : int =
245+
let re = Str.regexp_string needle in
246+
let rec loop pos acc =
247+
match Str.search_forward re hay pos with
248+
| exception Not_found -> acc
249+
| i -> loop (i + String.length needle) (acc + 1)
250+
in
251+
loop 0 0
252+
253+
let test_deno_no_duplicate_option_ctor () =
254+
match Parse_driver.parse_string ~file:"<localopt>" local_option_src with
255+
| exception e ->
256+
Alcotest.failf "local-option parse raised: %s" (Printexc.to_string e)
257+
| prog ->
258+
(match pipeline_to_deno prog with
259+
| Error m -> Alcotest.failf "deno codegen failed: %s" m
260+
| Ok js ->
261+
Alcotest.(check int)
262+
"`const Some` declared exactly once (preamble only, not re-emitted)"
263+
1 (count_substr "const Some" js))
264+
265+
let deno_dup_ctor_tests =
266+
[ Alcotest.test_case "declared Option does not duplicate preamble ctor (Deno)"
267+
`Quick test_deno_no_duplicate_option_ctor ]
268+
228269
let tests =
229270
[ ("STAGE-A AOT smoke (#136)", aot_smoke_tests);
230271
("STAGE-A multi-module integration (#137)", integration_tests);
231-
("cross-module constructor linking, Wasm (#138)", xmod_constructor_tests) ]
272+
("cross-module constructor linking, Wasm (#138)", xmod_constructor_tests);
273+
("Deno-ESM no duplicate Option/Result constructor", deno_dup_ctor_tests) ]

0 commit comments

Comments
 (0)