Skip to content

Commit e950dbc

Browse files
fix(typecheck): instantiate generic fn type params; prelude mut (#135 slice 7) (#163)
#135 slice 7 — root cause was NOT empty-array literals (those compile). It was a fundamental let-polymorphism bug: `check_fn_decl` never registered a function's explicit `<T>` type parameters, so an uppercase param `x: T` lowered via `lower_type_expr`'s `TyCon` fallthrough to a *rigid* `TCon "T"`. `generalize` ignores `TCon`, so every generic top-level function got `sc_tyvars = []` — effectively monomorphic. The first call pinned `T`; any second instantiation (or the very first against a concrete arg) failed `Unify.TypeMismatch (T, Int)`, and `use prelude::{…}` import-checks failed transitively (result/option/…). Fix (mirrors the existing let-generalization discipline): in `check_fn_decl`, before lowering, `enter_level` and register each `fd_type_params` name in `ctx.type_env` as a fresh tyvar at the deeper level; `exit_level` before the existing `generalize`, so those vars (level > ctx.level) become the scheme's quantified `sc_tyvars`. Scoped save/restore of `type_env` so params don't leak. Applied to both the extern and normal branches. Also fixes genuine stdlib bugs surfaced once typecheck passed: prelude.affine `map`/`filter`/`range`/`repeat`/`fold` reassigned immutable `let` bindings (`result`/`i`/`acc`) → `let mut`. With both, **prelude.affine now compiles end-to-end** (resolve→typecheck→codegen). Verification: `id<T>` instantiated at Int+Bool, generic `fold` from a monomorphic Int `sum`, monomorphic fns — all compile; the fundamental change regressed **none** of the suite. 2 regression tests; full suite green (230). Unblocks the typecheck wall for prelude (and, once visibility/slice 8 lands, result/option/collections via `use prelude`). Advances #135. Refs #128, #135. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 289b298 commit e950dbc

3 files changed

Lines changed: 55 additions & 8 deletions

File tree

lib/typecheck.ml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,28 @@ let register_builtins (ctx : context) : unit =
13121312

13131313
(** Check a top-level function declaration. *)
13141314
let check_fn_decl (ctx : context) (fd : fn_decl) : unit result =
1315+
(* #135 slice 7: register the explicit `<T>` type parameters as fresh,
1316+
generalizable unification variables before lowering param/return
1317+
types. Without this, an uppercase param like `x: T` lowered to a
1318+
*rigid* `TCon "T"` (lower_type_expr TyCon fallthrough), which
1319+
`generalize` ignores — so every generic top-level function was
1320+
effectively monomorphic and the second instantiation blew up with
1321+
`Unify.TypeMismatch (T, Int)` (and `use prelude::{…}` import-checks
1322+
failed transitively). Mirrors the let-generalization discipline:
1323+
enter a deeper level, create the vars there, generalize at the outer
1324+
level so they become the scheme's quantified tyvars. *)
1325+
let tp_names = List.map (fun (tp : type_param) -> tp.tp_name.name)
1326+
fd.fd_type_params in
1327+
let saved_tp = List.map (fun n -> (n, Hashtbl.find_opt ctx.type_env n))
1328+
tp_names in
1329+
enter_level ctx;
1330+
List.iter (fun n ->
1331+
Hashtbl.replace ctx.type_env n (fresh_tyvar ctx.level)) tp_names;
1332+
let restore_tp () =
1333+
List.iter (fun (n, old) -> match old with
1334+
| Some t -> Hashtbl.replace ctx.type_env n t
1335+
| None -> Hashtbl.remove ctx.type_env n) saved_tp
1336+
in
13151337
(* Extern functions have no body — register the signature so callers can
13161338
typecheck against it, then bail out before the body-check pass. *)
13171339
if fd.fd_body = FnExtern then begin
@@ -1340,8 +1362,10 @@ let check_fn_decl (ctx : context) (fd : fn_decl) : unit result =
13401362
in
13411363
TArrow (param_ty, q, acc, fn_eff)
13421364
) param_tys fd.fd_params ret_ty in
1365+
exit_level ctx;
13431366
let sc = generalize ctx fn_ty in
13441367
bind_scheme ctx fd.fd_name.name sc;
1368+
restore_tp ();
13451369
Ok ()
13461370
end
13471371
else
@@ -1396,9 +1420,13 @@ let check_fn_decl (ctx : context) (fd : fn_decl) : unit result =
13961420
| Some sc -> Hashtbl.replace ctx.name_types n sc
13971421
| None -> Hashtbl.remove ctx.name_types n
13981422
) old;
1399-
(* Generalize and rebind the function with its polymorphic type *)
1423+
(* Generalize and rebind the function with its polymorphic type.
1424+
exit_level first so the `<T>` type-param vars (created at the deeper
1425+
level above) are quantified by `generalize` (#135 slice 7). *)
1426+
exit_level ctx;
14001427
let sc = generalize ctx fn_ty in
14011428
bind_scheme ctx fd.fd_name.name sc;
1429+
restore_tp ();
14021430
Ok ()
14031431

14041432
(** Register a type declaration in the context. *)

stdlib/prelude.affine

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ type Result<T, E> = Ok(T) | Err(E)
2525
// ============================================================================
2626

2727
fn map<T, U>(arr: [T], f: T -> U) -> [U] {
28-
let result = [];
28+
let mut result = [];
2929
for x in arr {
3030
result = result ++ [f(x)];
3131
}
3232
result
3333
}
3434

3535
fn filter<T>(arr: [T], predicate: T -> Bool) -> [T] {
36-
let result = [];
36+
let mut result = [];
3737
for x in arr {
3838
if predicate(x) {
3939
result = result ++ [x];
@@ -43,7 +43,7 @@ fn filter<T>(arr: [T], predicate: T -> Bool) -> [T] {
4343
}
4444

4545
fn fold<T, U>(arr: [T], init: U, f: (U, T) -> U) -> U {
46-
let acc = init;
46+
let mut acc = init;
4747
for x in arr {
4848
acc = f(acc, x);
4949
}
@@ -121,8 +121,8 @@ fn any(arr: [Bool]) -> Bool {
121121
// ============================================================================
122122

123123
fn range(start: Int, end: Int) -> [Int] {
124-
let result = [];
125-
let i = start;
124+
let mut result = [];
125+
let mut i = start;
126126
while i < end {
127127
result = result ++ [i];
128128
i = i + 1;
@@ -131,8 +131,8 @@ fn range(start: Int, end: Int) -> [Int] {
131131
}
132132

133133
fn repeat<T>(value: T, n: Int) -> [T] {
134-
let result = [];
135-
let i = 0;
134+
let mut result = [];
135+
let mut i = 0;
136136
while i < n {
137137
result = result ++ [value];
138138
i = i + 1;

test/test_e2e.ml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3363,6 +3363,23 @@ let test_trait_sig_and_assoc_not_regressed () =
33633363
pub fn next(mut self) -> Option<Int>;
33643364
}|})
33653365

3366+
(* Issue #135 slice 7: top-level generic functions must instantiate their
3367+
`<T>` scheme with fresh vars per call. Before, `<T>` lowered to a rigid
3368+
`TCon "T"` that `generalize` ignored, so any 2nd instantiation failed
3369+
with `Unify.TypeMismatch (T, Int)` (and `use prelude` import-checks
3370+
failed transitively). *)
3371+
let test_generic_fn_multi_instantiation () =
3372+
Alcotest.(check bool) "id<T> called at Int and Bool in one program" true
3373+
(parse_check_passes
3374+
{|fn id<T>(x: T) -> T { return x; }
3375+
fn use_it() -> Bool { let a = id(1); let b = id(true); return b; }|})
3376+
3377+
let test_generic_hof_monomorphic_caller () =
3378+
Alcotest.(check bool) "generic fold<T,U> called by monomorphic Int sum" true
3379+
(parse_check_passes
3380+
{|fn fold<T, U>(arr: [T], init: U, f: (U, T) -> U) -> U { return init; }
3381+
fn sum(a: [Int]) -> Int { return fold(a, 0, fn(acc, x) => acc + x); }|})
3382+
33663383
let test_multi_arg_arrow () =
33673384
Alcotest.(check bool) "(A, B) -> C parses + typechecks" true
33683385
(parse_check_passes
@@ -3418,6 +3435,8 @@ let type_syntax_sugar_tests = [
34183435
Alcotest.test_case "effect E; + -> T / E (#135 slice 3)" `Quick test_bare_effect_and_effect_row;
34193436
Alcotest.test_case "trait default body + ref self (#135 sl5)" `Quick test_trait_default_body;
34203437
Alcotest.test_case "trait sig + assoc non-regressed (#135 sl5)" `Quick test_trait_sig_and_assoc_not_regressed;
3438+
Alcotest.test_case "generic fn multi-instantiation (#135 sl7)" `Quick test_generic_fn_multi_instantiation;
3439+
Alcotest.test_case "generic HOF + mono caller (#135 sl7)" `Quick test_generic_hof_monomorphic_caller;
34213440
Alcotest.test_case "(A, B) -> C (multi-arg arrow)" `Quick test_multi_arg_arrow;
34223441
Alcotest.test_case "(A, B) without arrow remains tuple" `Quick test_tuple_type_still_works;
34233442
]

0 commit comments

Comments
 (0)