Skip to content

Commit 2efe0e3

Browse files
fix(codegen-deno): don't re-declare preamble Option/Result constructors (#606)
## Deno-ESM: stop re-declaring the preamble's Option/Result constructors The locally-declared sibling of the duplicate-constructor bug fixed in #604. ### Bug The Deno-ESM runtime preamble always declares `Some`/`None`/`Ok`/`Err`. `gen_type_decl` *also* emits them for any program that **declares** `type Option`/`type Result` — including `stdlib/prelude.affine` — so the emitted module crashes under node: ``` $ affinescript compile --deno-esm -o prelude.deno.js stdlib/prelude.affine $ node prelude.deno.js SyntaxError: Identifier 'Some' has already been declared ``` It stayed latent because the #136 AOT smoke only checks the emitted module is **non-empty** — it never runs it. ### Fix Skip the variants the preamble already provides (`Some`/`None`/`Ok`/`Err`) when lowering a `TyEnum` in `codegen_deno.ml`. User-defined enums are unaffected. ### Verified ``` stdlib/prelude.affine -> deno : `const Some` ×1, runs under node ✅ (was ×2, crashed) user enum (Color=Red|Green|Blue): still emitted, runs ✅ tools/run_codegen_deno_tests.sh : all harnesses pass under node ✅ dune test : 459 green (+1 new regression test) ``` New test `Deno-ESM no duplicate Option/Result constructor` asserts `const Some` is declared exactly once — the run-under-node guard the AOT smoke lacked. ### Scope / follow-ups - This + #604 close the duplicate-constructor class on the **Deno** backend (the one executed in CI). - The **JS and C** backends show the same latent preamble/declaration duplication (2 `Some` decls) but their output isn't executed in CI — tracked as a follow-up, not fixed here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8 --- _Generated by [Claude Code](https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent c7922cf commit 2efe0e3

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)