diff --git a/docs/history/MODULE-SYSTEM-PROGRESS.md b/docs/history/MODULE-SYSTEM-PROGRESS.md index f25be1e..6543536 100644 --- a/docs/history/MODULE-SYSTEM-PROGRESS.md +++ b/docs/history/MODULE-SYSTEM-PROGRESS.md @@ -210,7 +210,8 @@ type context = { | Visibility checking | ✅ | Public/PubCrate filtering | | Symbol registration | ✅ | Symbols added to table | | Type information transfer | ✅ | **FIXED** | -| Re-exports | ❌ | Not implemented | +| Cross-module constructor codegen | ✅ | Directly-imported enum constructors (`use prelude::{Option, Some, None}`) lower on every backend (#138) | +| Re-exports (transitive) | ❌ | A module surfacing names it itself imported (`use option` → prelude's `Option`) — not implemented | | Nested modules | ❌ | Not implemented | ## Known Limitations @@ -400,3 +401,29 @@ actual objective of #128. | #138 | Delete the b895374 seeded-builtins band-aid once the prelude re-export module exists. | No code change in #132 (decision + documentation only). + +### #138 codegen follow-up (2026-06-20) + +Removing the `b895374` seeded `Some/None/Ok/Err` builtins (front-end half of +#138) correctly routed those constructors through the module path, so `check` +passes — but it surfaced a codegen gap: a consumer that imports prelude's +`Option`/`Result` and applies their constructors type-checked yet failed to +compile, because the backends learn variant tags only from `TopType` decls and +imported types never reached them. + +- **Core-Wasm backend** (`Codegen.gen_imports`): wired up only `TopFn` + (→ wasm import) and `TopConst` (→ global); imported types were dropped. It now + also registers the constructor tags / struct layouts of imported public types, + reusing the local-type registration in `gen_decl`. +- **Other backends** (Deno / JS / Julia / C / Rust / …): `Module_loader.flatten_imports` + now inlines imported public `TopType` decls (a separate namespace from + fn/const, local-wins, deduped) so the `prog_decls`-iterating codegens see them. + +Scope: **directly-imported** constructors lower on every backend. **Transitive +re-export** (a module re-exposing constructors it itself imported) remains +unimplemented — see the status table above. Unrelated and still open: the +core-Wasm pattern-codegen gap for tuple patterns (`UnsupportedFeature "Only +variable and wildcard patterns supported in tuple patterns"`, which +`stdlib/option.affine` / `result.affine` hit) and the mixed-representation match +of a zero-arg variant against a constructor-with-args arm — both reproduce with +purely local enums and are independent of cross-module linking. diff --git a/lib/codegen.ml b/lib/codegen.ml index 2b60b74..d6a65ce 100644 --- a/lib/codegen.ml +++ b/lib/codegen.ml @@ -3339,7 +3339,65 @@ let gen_imports (loader : Module_loader.t) (imports : import_decl list) (ctx : c | _ -> None ) lm.mod_program.prog_decls) in + (* #138: register the constructor tags (and struct field layouts) of + imported PUBLIC types. [gen_imports] is the WASM backend's native + cross-module import path; it historically wired up only [TopFn] + (-> wasm import) and [TopConst] (-> global), silently dropping imported + TYPES. Codegen learns variant tags ONLY from [TopType] decls (see + [gen_decl]/[variant_tags]), and the WASM compile path feeds the original + (un-flattened) [prog] to [generate_module], so applying an imported + constructor such as [Some]/[None] from `use prelude::{Option, Some, None}` + raised [UnboundVariable] at codegen even though resolve + typecheck + passed. We reuse the local-type registration in [gen_decl] so imported + and local types share exactly one code path; the non-WASM backends get + the same decls inlined via [Module_loader.flatten_imports]. *) + let register_imported_types ctx = + (* Dedup imported types by name across all imports (a type may be reachable + through more than one path). Local [TopType]s — registered afterwards by + the [prog_decls] fold in [generate_module] — are prepended after these, + so they win on the first-match [List.assoc] lookup. *) + let seen = Hashtbl.create 8 in + let path_strs path = List.map (fun (id : ident) -> id.name) path in + List.fold_left (fun acc imp -> + let* ctx = acc in + let p = match imp with + | ImportSimple (path, _) | ImportList (path, _) | ImportGlob path -> + path_strs path + in + match Module_loader.load_module loader p with + | Error _ -> Ok ctx + | Ok lm -> + let public_types = List.filter_map (function + | TopType td when td.td_vis = Public || td.td_vis = PubCrate -> Some td + | _ -> None + ) lm.mod_program.prog_decls in + (* `use M::{..}` selects a type when the list names the type itself or + any of its constructors (so `use prelude::{Some}` works without also + naming `Option`); `use M` / `use M::*` bring all public types. *) + let selected = match imp with + | ImportGlob _ | ImportSimple _ -> public_types + | ImportList (_, items) -> + let wanted = List.map (fun (it : import_item) -> it.ii_name.name) items in + List.filter (fun td -> + List.mem td.td_name.name wanted || + (match td.td_body with + | TyEnum variants -> + List.exists (fun vd -> List.mem vd.vd_name.name wanted) variants + | _ -> false) + ) public_types + in + List.fold_left (fun acc td -> + let* ctx = acc in + if Hashtbl.mem seen td.td_name.name then Ok ctx + else begin + Hashtbl.add seen td.td_name.name (); + gen_decl ctx (TopType td) + end + ) (Ok ctx) selected + ) (Ok ctx) imports + in let entries = List.concat_map expand_import imports in + let* ctx = register_imported_types ctx in List.fold_left (fun acc e -> let* ctx = acc in process_one ctx e diff --git a/lib/module_loader.ml b/lib/module_loader.ml index 9ea5caf..a0a5351 100644 --- a/lib/module_loader.ml +++ b/lib/module_loader.ml @@ -345,4 +345,61 @@ let flatten_imports (loader : t) (prog : program) : program = ) select ) prog.prog_imports in - { prog with prog_decls = imported_decls @ prog.prog_decls } + (* #138: type declarations are a SEPARATE namespace from fn/const bindings, + so they get their own dedup table — a local `fn Foo` must not suppress an + imported `type Foo`, and vice versa. Imported public types are inlined too + so the prog_decls-iterating backends (Deno / JS / Julia / C / Rust / ...) + register their constructors exactly as they would for a local type; + without this, applying an imported constructor (`Some`/`None` from + `use prelude::{Option, Some, None}`) can reach codegen with no type in + scope. Local types win over imported ones, and a type reachable through + more than one path is carried only once. The Wasm backend gets the same + registration natively in [Codegen.gen_imports]. *) + let local_type_names = + List.filter_map (function + | TopType td -> Some td.td_name.name + | _ -> None + ) prog.prog_decls + in + let type_already_in = Hashtbl.create 16 in + List.iter (fun n -> Hashtbl.add type_already_in n ()) local_type_names; + let imported_types = + List.concat_map (fun imp -> + let path_strs path = List.map (fun (id : ident) -> id.name) path in + let mod_path = match imp with + | ImportSimple (p, _) | ImportList (p, _) | ImportGlob p -> path_strs p + in + match Hashtbl.find_opt loader.loaded mod_path with + | None -> [] + | Some lm -> + let public_types = List.filter_map (function + | TopType td when td.td_vis = Public || td.td_vis = PubCrate -> Some td + | _ -> None + ) lm.mod_program.prog_decls in + (* `use M::{..}` selects a type when the list names the type itself or + any of its constructors; `use M` / `use M::*` bring all public + types (the resolver still gates what is referenceable). *) + let selected = match imp with + | ImportGlob _ | ImportSimple _ -> public_types + | ImportList (_, items) -> + let wanted = List.map (fun (it : import_item) -> it.ii_name.name) items in + List.filter (fun td -> + List.mem td.td_name.name wanted || + (match td.td_body with + | TyEnum variants -> + List.exists (fun vd -> List.mem vd.vd_name.name wanted) variants + | _ -> false) + ) public_types + in + List.filter_map (fun td -> + if Hashtbl.mem type_already_in td.td_name.name then None + else begin + Hashtbl.add type_already_in td.td_name.name (); + Some (TopType td) + end + ) selected + ) prog.prog_imports + in + (* Types precede imported fns/consts and all local decls so the single-pass + codegen registers an imported type before any function that uses it. *) + { prog with prog_decls = imported_types @ imported_decls @ prog.prog_decls } diff --git a/test/test_stdlib_aot.ml b/test/test_stdlib_aot.ml index b853c66..7f94db1 100644 --- a/test/test_stdlib_aot.ml +++ b/test/test_stdlib_aot.ml @@ -61,6 +61,37 @@ let pipeline_to_deno (prog : Ast.program) : (string, string) result = | Error e -> Error (Printf.sprintf "deno-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 + linking (#138) the test below regression-locks. The Wasm path feeds the + original (un-flattened) [prog] to codegen and resolves imported decls + natively via [Codegen.gen_imports]. *) +let pipeline_to_wasm (prog : Ast.program) : (Wasm.wasm_module, 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 optimized = Opt.fold_constants_program prog in + (match Codegen.generate_module ~loader:ld optimized with + | Error e -> + Error (Printf.sprintf "wasm-codegen: %s" + (Codegen.show_codegen_error e)) + | Ok m -> Ok m))) + let parse_file_safe path = try Ok (Parse_driver.parse_file path) with @@ -140,6 +171,61 @@ let integration_tests = [ Alcotest.test_case "string+option+collections together" `Quick test_multi_module_integration ] +(* ---- #138: cross-module constructor linking on the core-Wasm backend ---- + + A consumer that imports prelude's Option/Result and APPLIES their + constructors must reach a runnable artifact. The front-end resolves + `Some`/`None`/`Ok`/`Err` through the module path, so `check` passes; before + the #138 codegen fix [Codegen.gen_imports] dropped the imported TYPE decls, + so the Wasm backend had no variant tag for `Some` and raised + [UnboundVariable "...Some"] at codegen. This feeds the imported-constructor + decl shape to all three [variant_tags] consumers in [Codegen]: construction + with an argument (`Some(x)`, `Ok`/`Err`), the zero-arg form (`None`), and + constructor patterns (`match`). It is the Wasm counterpart to the #137 + Deno-path integration above. *) +let imported_ctors_src = {| +module xmod_ctors; +use prelude::{ Option, Some, None, Result, Ok, Err }; + +pub fn wrap(x: Int) -> Option { Some(x) } +pub fn empty() -> Option { None } + +pub fn unwrap_or(o: Option, d: Int) -> Int { + match o { + Some(v) => v, + None => d, + } +} + +pub fn divide(a: Int, b: Int) -> Result { + if b == 0 { Err(0) } else { Ok(a / b) } +} +|} + +let test_imported_constructors_wasm () = + match Parse_driver.parse_string ~file:"" imported_ctors_src with + | exception e -> + Alcotest.failf "imported-ctors parse raised: %s" (Printexc.to_string e) + | prog -> + (match pipeline_to_wasm prog with + | Ok m -> + let names = + List.map (fun (e : Wasm.export) -> e.Wasm.e_name) m.Wasm.exports in + List.iter + (fun fn -> + Alcotest.(check bool) + (Printf.sprintf "Wasm module exports %s" fn) + true (List.mem fn names)) + [ "wrap"; "empty"; "unwrap_or"; "divide" ] + | Error m -> + Alcotest.failf + "imported prelude constructors must codegen to Wasm (#138): %s" m) + +let xmod_constructor_tests = + [ Alcotest.test_case "imported Option/Result constructors -> Wasm" `Quick + test_imported_constructors_wasm ] + let tests = [ ("STAGE-A AOT smoke (#136)", aot_smoke_tests); - ("STAGE-A multi-module integration (#137)", integration_tests) ] + ("STAGE-A multi-module integration (#137)", integration_tests); + ("cross-module constructor linking, Wasm (#138)", xmod_constructor_tests) ]