Skip to content

Commit 186fd8b

Browse files
hyperpolymathclaude
andcommitted
fix(resolve): two-pass — forward-declare top-level names (#135 slice 11)
#135 slice 11 (discovered during slice 8). The resolver was single-pass: `resolve_decl` defined a name then immediately resolved its body, and the program entry points folded `resolve_decl` over decls in source order. So any reference to a top-level function defined *later* in the file failed with `UndefinedVariable` — e.g. collections.affine `binary_search` → `binary_search_helper`, reproducible even without `module`. Mutual recursion was impossible. Fix: standard two-pass name resolution. New `pre_register_decl` / `pre_register_program` declares every top-level name (fn, type + enum constructors, effect + ops, trait, const) before any body is resolved; wired into all three entry points (`resolve_program`, `resolve_and_typecheck_module`, `resolve_program_with_loader`). `Symbol.define` is `Hashtbl.replace`-based so the existing in-body re-definition is idempotent — no double-define hazard. Effect: forward references and **mutual recursion** now resolve; collections.affine advances RESOLVE → TYPECHECK (a distinct later defect). Self-recursion verified non-regressed. 3 regression tests; full suite green (233), zero regressions from this resolver change. Advances #135. Refs #128, #135. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 582546a commit 186fd8b

2 files changed

Lines changed: 72 additions & 2 deletions

File tree

lib/resolve.ml

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,9 +488,52 @@ let rec resolve_decl (ctx : context) (decl : top_level) : unit result =
488488
Symbol.SKVariable tc.tc_name.span tc.tc_vis in
489489
resolve_expr ctx tc.tc_value
490490

491+
(** Pre-register a top-level declaration's *names* (no body resolution),
492+
so that forward references between top-level declarations resolve
493+
(issue #135 slice 11). Previously [resolve_decl] defined a name then
494+
immediately resolved its body, and [resolve_program] folded in source
495+
order — so `fn a() { b() } fn b() {}` failed with `UndefinedVariable
496+
b`. This is a standard two-pass: pass 1 declares every top-level
497+
name, pass 2 resolves bodies. [Symbol.define] is [Hashtbl.replace]
498+
based, so the re-definition inside [resolve_decl] is idempotent. *)
499+
let pre_register_decl (ctx : context) (decl : top_level) : unit =
500+
match decl with
501+
| TopFn fd ->
502+
ignore (Symbol.define ctx.symbols fd.fd_name.name
503+
Symbol.SKFunction fd.fd_name.span fd.fd_vis)
504+
| TopType td ->
505+
ignore (Symbol.define ctx.symbols td.td_name.name
506+
Symbol.SKType td.td_name.span td.td_vis);
507+
(match td.td_body with
508+
| TyEnum variants ->
509+
List.iter (fun (vd : variant_decl) ->
510+
ignore (Symbol.define ctx.symbols vd.vd_name.name
511+
Symbol.SKConstructor vd.vd_name.span td.td_vis)
512+
) variants
513+
| TyAlias _ | TyStruct _ | TyExtern -> ())
514+
| TopEffect ed ->
515+
ignore (Symbol.define ctx.symbols ed.ed_name.name
516+
Symbol.SKEffect ed.ed_name.span ed.ed_vis);
517+
List.iter (fun op ->
518+
ignore (Symbol.define ctx.symbols op.eod_name.name
519+
Symbol.SKEffectOp op.eod_name.span ed.ed_vis)
520+
) ed.ed_ops
521+
| TopTrait td ->
522+
ignore (Symbol.define ctx.symbols td.trd_name.name
523+
Symbol.SKTrait td.trd_name.span td.trd_vis)
524+
| TopConst tc ->
525+
ignore (Symbol.define ctx.symbols tc.tc_name.name
526+
Symbol.SKVariable tc.tc_name.span tc.tc_vis)
527+
| TopImpl _ -> ()
528+
529+
(** Run the forward-declaration pass over a whole program. *)
530+
let pre_register_program (ctx : context) (program : program) : unit =
531+
List.iter (pre_register_decl ctx) program.prog_decls
532+
491533
(** Resolve an entire program *)
492534
let resolve_program (program : program) : (context, resolve_error * Span.t) Result.t =
493535
let ctx = create_context () in
536+
pre_register_program ctx program;
494537
match List.fold_left (fun acc decl ->
495538
match acc with
496539
| Error e -> Error e
@@ -506,7 +549,9 @@ let resolve_and_typecheck_module (loaded_mod : Module_loader.loaded_module)
506549
let symbols = Symbol.create () in
507550
let mod_ctx = { symbols; current_module = []; imports = []; references = [] } in
508551

509-
(* Resolve all declarations in the module *)
552+
(* Pass 1: forward-declare every top-level name (#135 slice 11). *)
553+
pre_register_program mod_ctx prog;
554+
(* Pass 2: resolve all declaration bodies. *)
510555
let* () = List.fold_left (fun acc decl ->
511556
match acc with
512557
| Error e -> Error e
@@ -753,7 +798,9 @@ let resolve_program_with_loader
753798
let type_ctx = Typecheck.create_context ctx.symbols in
754799
(* First resolve imports using module loader *)
755800
let* () = resolve_imports_with_loader ctx type_ctx loader program.prog_imports in
756-
(* Then resolve declarations *)
801+
(* Pass 1: forward-declare every top-level name (#135 slice 11). *)
802+
pre_register_program ctx program;
803+
(* Pass 2: resolve declaration bodies. *)
757804
let* () = List.fold_left (fun acc decl ->
758805
match acc with
759806
| Error e -> Error e

test/test_e2e.ml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3380,6 +3380,26 @@ let test_generic_hof_monomorphic_caller () =
33803380
{|fn fold<T, U>(arr: [T], init: U, f: (U, T) -> U) -> U { return init; }
33813381
fn sum(a: [Int]) -> Int { return fold(a, 0, fn(acc, x) => acc + x); }|})
33823382

3383+
(* Issue #135 slice 11: the resolver is two-pass — all top-level names
3384+
are forward-declared before any body is resolved. Before, a call to
3385+
a function defined later in the file was `UndefinedVariable`. *)
3386+
let test_forward_reference () =
3387+
Alcotest.(check bool) "fn calling a later-defined fn resolves" true
3388+
(parse_check_passes
3389+
{|fn a() -> Int { return b(); }
3390+
fn b() -> Int { return 1; }|})
3391+
3392+
let test_mutual_recursion () =
3393+
Alcotest.(check bool) "mutually recursive fns resolve + typecheck" true
3394+
(parse_check_passes
3395+
{|fn even(n: Int) -> Bool { if n == 0 { return true; } return odd(n - 1); }
3396+
fn odd(n: Int) -> Bool { if n == 0 { return false; } return even(n - 1); }|})
3397+
3398+
let test_self_recursion_not_regressed () =
3399+
Alcotest.(check bool) "self-recursion still resolves" true
3400+
(parse_check_passes
3401+
{|fn rec(n: Int) -> Int { if n <= 0 { return 0; } return rec(n - 1); }|})
3402+
33833403
let test_multi_arg_arrow () =
33843404
Alcotest.(check bool) "(A, B) -> C parses + typechecks" true
33853405
(parse_check_passes
@@ -3437,6 +3457,9 @@ let type_syntax_sugar_tests = [
34373457
Alcotest.test_case "trait sig + assoc non-regressed (#135 sl5)" `Quick test_trait_sig_and_assoc_not_regressed;
34383458
Alcotest.test_case "generic fn multi-instantiation (#135 sl7)" `Quick test_generic_fn_multi_instantiation;
34393459
Alcotest.test_case "generic HOF + mono caller (#135 sl7)" `Quick test_generic_hof_monomorphic_caller;
3460+
Alcotest.test_case "forward reference resolves (#135 sl11)" `Quick test_forward_reference;
3461+
Alcotest.test_case "mutual recursion (#135 sl11)" `Quick test_mutual_recursion;
3462+
Alcotest.test_case "self-recursion non-regressed (#135 sl11)" `Quick test_self_recursion_not_regressed;
34403463
Alcotest.test_case "(A, B) -> C (multi-arg arrow)" `Quick test_multi_arg_arrow;
34413464
Alcotest.test_case "(A, B) without arrow remains tuple" `Quick test_tuple_type_still_works;
34423465
]

0 commit comments

Comments
 (0)