Skip to content

Commit fc92cc5

Browse files
fix(resolve): remove b895374 Some/None/Ok/Err seed; resolve imports recursively (#138) (#193)
The b895374/#122 band-aid seeded Some/None/Ok/Err as flat builtins so files resolved without importing prelude — entrenching the interpreter- era flat namespace (#138, part of #128). Root cause it masked: resolve_and_typecheck_module (the path used when a module is pulled in via `use`) resolved a dependency's decls but never its OWN imports, so an imported module couldn't reach prelude constructors / sibling modules on its own. The global seed hid this. - resolve.ml: drop Some/None/Ok/Err from seed_builtins (keep RuntimeError — genuine interpreter exception variant, no module home). - resolve.ml: make resolve_and_typecheck_module / resolve_imports_with_loader mutually recursive so an imported module resolves its own `use prelude::{...}` / `use string::{...}` through the loader. The stdlib import graph is an acyclic DAG (max depth io->string->prelude). - typecheck.ml: drop the mirrored Some/None/Ok/Err builtin schemes (keep RuntimeError, keep opt/res type-ctor helpers for builtin sigs); remove the stray "If without else returns…" debug eprintf. - string/io/testing.affine: add the proper `use prelude::{...}` imports they were relying on the seed for (ADR-011). - test_e2e: de-couple the nested-generic return-position test from the seed (exercise the type syntax without constructing Ok/None), like its param-position siblings. stdlib 19/19 via the proper module path; 233/233 dune test; zero regression. Refs #128 Refs #138 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 58c8eb8 commit fc92cc5

6 files changed

Lines changed: 82 additions & 81 deletions

File tree

lib/resolve.ml

Lines changed: 59 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,14 @@ let seed_builtins (symbols : Symbol.t) : unit =
8888
let defc name =
8989
let _ = Symbol.define symbols name SKConstructor Span.dummy Public in ()
9090
in
91-
defc "Some"; defc "None"; defc "Ok"; defc "Err";
91+
(* #138: Some/None/Ok/Err are no longer seeded as flat builtins — the
92+
stdlib now reaches them through the proper prelude/module path
93+
(`use prelude::{Some, None, Ok, Err}`; prelude.affine itself
94+
defines `Option`/`Result`). Removing the b895374 band-aid keeps it
95+
from becoming load-bearing. *)
9296
(* Interpreter builtin exception variant, pattern-matched in try/catch
93-
arms by the honest stdlib (testing.affine, result.affine). *)
97+
arms by the honest stdlib (testing.affine, result.affine). It has
98+
no module home by design, so it remains a genuine builtin. *)
9499
defc "RuntimeError"
95100

96101
(** Create a new resolution context, pre-seeded with builtins. *)
@@ -554,41 +559,9 @@ let resolve_program (program : program) : (context, resolve_error * Span.t) Resu
554559
| Ok () -> Ok ctx
555560
| Error e -> Error e
556561

557-
(** Resolve and type-check a loaded module's symbols *)
558-
let resolve_and_typecheck_module (loaded_mod : Module_loader.loaded_module)
559-
: (Symbol.t * Typecheck.context) result =
560-
let prog = Module_loader.get_program loaded_mod in
561-
let symbols = Symbol.create () in
562-
seed_builtins symbols;
563-
let mod_ctx = { symbols; current_module = []; imports = []; references = [] } in
564-
565-
(* Pass 1: forward-declare every top-level name (#135 slice 11). *)
566-
pre_register_program mod_ctx prog;
567-
(* Pass 2: resolve all declaration bodies. *)
568-
let* () = List.fold_left (fun acc decl ->
569-
match acc with
570-
| Error e -> Error e
571-
| Ok () -> resolve_decl mod_ctx decl
572-
) (Ok ()) prog.prog_decls in
573-
574-
(* Type-check all declarations. Seed builtin schemes (len, arithmetic,
575-
Some/None/…) exactly as Typecheck.check_program does for the top-level
576-
program — the manual check_decl fold below otherwise leaves an imported
577-
module's use of builtins as "Unbound variable". *)
578-
let type_ctx = Typecheck.create_context symbols in
579-
Typecheck.register_builtins type_ctx;
580-
let type_result = List.fold_left (fun acc decl ->
581-
match acc with
582-
| Error e -> Error e
583-
| Ok () -> Typecheck.check_decl type_ctx decl
584-
) (Ok ()) prog.prog_decls in
585-
586-
match type_result with
587-
| Ok () -> Ok (symbols, type_ctx)
588-
| Error type_err ->
589-
(* Convert type error to resolve error *)
590-
let msg = Typecheck.show_type_error type_err in
591-
Error (ImportError ("Type checking failed: " ^ msg), Span.dummy)
562+
(* [resolve_and_typecheck_module] is defined below, mutually recursive
563+
with [resolve_imports_with_loader], so that an imported module's own
564+
`use` imports are resolved through the loader (no flat builtin seed). *)
592565

593566
(** Look up a scheme for [sym] in [source_types] (sym_id-keyed) first, then
594567
fall back to [source_name_types] (name-keyed). The fallback is needed
@@ -664,7 +637,52 @@ let import_specific_items
664637
) (Ok ()) items
665638

666639
(** Resolve imports in a program using module loader *)
667-
let resolve_imports_with_loader
640+
let rec resolve_and_typecheck_module
641+
(loader : Module_loader.t)
642+
(loaded_mod : Module_loader.loaded_module)
643+
: (Symbol.t * Typecheck.context) result =
644+
let prog = Module_loader.get_program loaded_mod in
645+
let symbols = Symbol.create () in
646+
seed_builtins symbols;
647+
let mod_ctx = { symbols; current_module = []; imports = []; references = [] } in
648+
(* Type-check context, created up front so this module's own imports
649+
can populate its scheme maps before its decls are checked. *)
650+
let type_ctx = Typecheck.create_context symbols in
651+
Typecheck.register_builtins type_ctx;
652+
653+
(* Resolve THIS module's own `use` imports first (#138 / #128
654+
coherence). A dependency module reaches Some/None/Ok/Err and its
655+
sibling modules through its own `use prelude::{...}` /
656+
`use string::{...}`, not a flat builtin seed. Mutually recursive
657+
with the loader; the stdlib import graph is an acyclic DAG
658+
(max depth io -> string -> prelude). *)
659+
let* () =
660+
resolve_imports_with_loader mod_ctx type_ctx loader prog.prog_imports
661+
in
662+
663+
(* Pass 1: forward-declare every top-level name (#135 slice 11). *)
664+
pre_register_program mod_ctx prog;
665+
(* Pass 2: resolve all declaration bodies. *)
666+
let* () = List.fold_left (fun acc decl ->
667+
match acc with
668+
| Error e -> Error e
669+
| Ok () -> resolve_decl mod_ctx decl
670+
) (Ok ()) prog.prog_decls in
671+
672+
(* Type-check all declarations. *)
673+
let type_result = List.fold_left (fun acc decl ->
674+
match acc with
675+
| Error e -> Error e
676+
| Ok () -> Typecheck.check_decl type_ctx decl
677+
) (Ok ()) prog.prog_decls in
678+
679+
match type_result with
680+
| Ok () -> Ok (symbols, type_ctx)
681+
| Error type_err ->
682+
let msg = Typecheck.show_type_error type_err in
683+
Error (ImportError ("Type checking failed: " ^ msg), Span.dummy)
684+
685+
and resolve_imports_with_loader
668686
(ctx : context)
669687
(type_ctx : Typecheck.context)
670688
(loader : Module_loader.t)
@@ -678,7 +696,7 @@ let resolve_imports_with_loader
678696
begin match Module_loader.load_module loader path_strs with
679697
| Ok loaded_mod ->
680698
(* Resolve and type-check the module *)
681-
begin match resolve_and_typecheck_module loaded_mod with
699+
begin match resolve_and_typecheck_module loader loaded_mod with
682700
| Ok (mod_symbols, mod_type_ctx) ->
683701
let alias_str = Option.map (fun id -> id.name) alias in
684702
import_resolved_symbols ctx.symbols
@@ -704,7 +722,7 @@ let resolve_imports_with_loader
704722
begin match Module_loader.load_module loader path_strs with
705723
| Ok loaded_mod ->
706724
(* Resolve and type-check the module *)
707-
begin match resolve_and_typecheck_module loaded_mod with
725+
begin match resolve_and_typecheck_module loader loaded_mod with
708726
| Ok (mod_symbols, mod_type_ctx) ->
709727
import_specific_items ctx.symbols
710728
type_ctx.Typecheck.var_types
@@ -728,7 +746,7 @@ let resolve_imports_with_loader
728746
begin match Module_loader.load_module loader path_strs with
729747
| Ok loaded_mod ->
730748
(* Resolve and type-check the module *)
731-
begin match resolve_and_typecheck_module loaded_mod with
749+
begin match resolve_and_typecheck_module loader loaded_mod with
732750
| Ok (mod_symbols, mod_type_ctx) ->
733751
(* Import all public symbols *)
734752
Hashtbl.iter (fun _id sym ->

lib/typecheck.ml

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,6 @@ let rec synth (ctx : context) (expr : expr) : ty result =
639639
let* () = unify_or_err then_ty else_ty in
640640
Ok then_ty
641641
| None ->
642-
let () =
643-
if ty_to_string then_ty <> ty_to_string ty_unit then
644-
Format.eprintf "If without else returns %s; then=%s cond=%s\n%!"
645-
(ty_to_string then_ty) (expr_summary ei_then) (expr_summary ei_cond)
646-
else
647-
()
648-
in
649642
(* No else branch: result is Unit *)
650643
let* () = unify_or_err then_ty ty_unit in
651644
Ok ty_unit
@@ -1284,46 +1277,25 @@ let register_builtins (ctx : context) : unit =
12841277
(issue #122 v2.5). Concrete String/Char types; the Deno-ESM backend
12851278
lowers each to a JS intrinsic. char ::= TCon "Char". *)
12861279
let ty_char = TCon "Char" in
1280+
(* Option/Result type constructors, used only to spell the *types* of
1281+
builtin signatures (parse_int, read_file, …) — not the value
1282+
constructors. *)
12871283
let opt t = TApp (TCon "Option", [t]) in
1288-
(* Option / Result value constructors — seeded as polymorphic builtin
1289-
schemes so the honest stdlib resolves without the legacy prelude
1290-
(mirrors the Resolve.create_context constructor seeding). Codegen
1291-
(codegen_deno.ml:111-114) already provides the Some/None/Ok/Err
1292-
runtime; this is purely the front-end type side. A user/prelude
1293-
`type Option`/`type Result` decl shadows these. Issue #122. *)
12941284
let res a b = TApp (TCon "Result", [a; b]) in
1285+
(* #138: Some/None/Ok/Err are no longer seeded as polymorphic builtin
1286+
schemes. The stdlib now resolves them through the proper module
1287+
path — prelude.affine declares `type Option`/`type Result` and
1288+
every consumer does `use prelude::{Some, None, Ok, Err}`. Removing
1289+
the b895374 / #122 front-end band-aid keeps it from becoming
1290+
load-bearing now that real resolution + the module model have
1291+
landed (stdlib 19/19). Codegen still provides the runtime. *)
12951292
let fresh_named () =
12961293
let tv = fresh_tyvar 0 in
12971294
let v = (match tv with
12981295
| TVar r -> (match !r with Unbound (v, _) -> v | _ -> assert false)
12991296
| _ -> assert false) in
13001297
(v, tv)
13011298
in
1302-
let (v_none, t_none) = fresh_named () in
1303-
bind_scheme ctx "None"
1304-
{ sc_tyvars = [(v_none, Types.KType)]; sc_effvars = []; sc_rowvars = [];
1305-
sc_body = opt t_none };
1306-
Hashtbl.replace ctx.constructor_env "None" (opt t_none);
1307-
let (v_some, t_some) = fresh_named () in
1308-
let some_ty = TArrow (t_some, QOmega, opt t_some, EPure) in
1309-
bind_scheme ctx "Some"
1310-
{ sc_tyvars = [(v_some, Types.KType)]; sc_effvars = []; sc_rowvars = [];
1311-
sc_body = some_ty };
1312-
Hashtbl.replace ctx.constructor_env "Some" some_ty;
1313-
let (v_oka, t_oka) = fresh_named () in
1314-
let (v_okb, t_okb) = fresh_named () in
1315-
let ok_ty = TArrow (t_oka, QOmega, res t_oka t_okb, EPure) in
1316-
bind_scheme ctx "Ok"
1317-
{ sc_tyvars = [(v_oka, Types.KType); (v_okb, Types.KType)];
1318-
sc_effvars = []; sc_rowvars = []; sc_body = ok_ty };
1319-
Hashtbl.replace ctx.constructor_env "Ok" ok_ty;
1320-
let (v_erra, t_erra) = fresh_named () in
1321-
let (v_errb, t_errb) = fresh_named () in
1322-
let err_ty = TArrow (t_errb, QOmega, res t_erra t_errb, EPure) in
1323-
bind_scheme ctx "Err"
1324-
{ sc_tyvars = [(v_erra, Types.KType); (v_errb, Types.KType)];
1325-
sc_effvars = []; sc_rowvars = []; sc_body = err_ty };
1326-
Hashtbl.replace ctx.constructor_env "Err" err_ty;
13271299
(* [RuntimeError(String)] is the interpreter's builtin exception variant
13281300
(see [Interp]: panics surface as [VVariant ("RuntimeError", VString
13291301
msg)]). The honest stdlib pattern-matches it in [try/catch] arms

stdlib/io.affine

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
// show(value) -> String
2121
// time_now() -> Float (CPU time in seconds)
2222

23-
// Cross-module import (ADR-011: explicit `use module::{...}`)
23+
// Cross-module imports (ADR-011: explicit `use module::{...}`)
24+
use prelude::{ Option, Result, Some, None, Ok, Err };
2425
use string::{ split, join };
2526

2627
// ============================================================================

stdlib/string.affine

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
// int_to_char(n) -> Char (ASCII code point to character)
1919
// show(v) -> String (any value to debug string)
2020

21+
// Cross-module import (ADR-011: explicit `use module::{...}`)
22+
use prelude::{ Option, Some, None };
23+
2124
// ============================================================================
2225
// String inspection
2326
// ============================================================================

stdlib/testing.affine

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
//
99
// Depends on builtins: show, panic, time_now
1010

11+
// Cross-module import (ADR-011: explicit `use module::{...}`)
12+
use prelude::{ Option, Result, Some, None, Ok, Err };
13+
1114
// ============================================================================
1215
// Assertions
1316
// ============================================================================

test/test_e2e.ml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3426,9 +3426,13 @@ let test_angle_nested_gtgtgt () =
34263426
{|fn f(o: Option<Option<Result<Int, String>>>) -> Int { return 0; }|})
34273427

34283428
let test_angle_nested_return_pos () =
3429+
(* Exercises the nested generic *in return position* (the point of this
3430+
test) without constructing Ok/None — those live in prelude and are
3431+
reached via `use prelude::{...}`, not a flat builtin seed (#138).
3432+
Mirrors the param-position sibling tests above. *)
34293433
Alcotest.(check bool) "-> Result<Option<T>, E> nested in return position" true
34303434
(parse_check_passes
3431-
{|fn f() -> Result<Option<Int>, String> { return Ok(None); }|})
3435+
{|fn f(r: Result<Option<Int>, String>) -> Result<Option<Int>, String> { return r; }|})
34323436

34333437
(* Non-regression: a real right-shift expression must still be one GTGT,
34343438
not split, since GTGT is grammatical there. *)

0 commit comments

Comments
 (0)