Skip to main content

aver/ir/hir/
resolve.rs

1//! `Expr → ResolvedExpr` conversion pass.
2//!
3//! Walks the post-typecheck AST and produces the resolved HIR shape
4//! defined in [`crate::ir::hir`]. Identity is the only thing that
5//! changes — the lowering is one-to-one in structure, evaluation
6//! order, and effect classification.
7//!
8//! ## What the resolver does
9//!
10//! 1. Classifies every `Expr::FnCall(callee, args)` callee into a
11//!    [`ResolvedCallee`] — user `FnId`, builtin namespace method,
12//!    local lambda slot, or `Unresolved` passthrough.
13//! 2. Resolves every `Expr::Constructor(name, …)` (and matching
14//!    [`crate::ast::Pattern::Constructor`]) into a [`ResolvedCtor`]
15//!    — user `CtorId` + owning `TypeId`, the four built-in
16//!    Result/Option variants, or `Unresolved` passthrough.
17//! 3. Resolves every `Expr::RecordCreate` / `Expr::RecordUpdate`
18//!    `type_name` to a `TypeId` against the program's `SymbolTable`
19//!    in the current module's resolver context (peer review round 6
20//!    on #148 — owner-aware lookup).
21//! 4. Resolves every `Expr::TailCall(data)` target name to a `FnId`.
22//! 5. Lifts `Stmt::Binding(name, Option<String>, expr)` to
23//!    [`ResolvedStmt::Binding`] with an `Option<Type>` annotation
24//!    (parsed + canonicalised through the same owner-aware
25//!    resolver context).
26//!
27//! ## What the resolver does NOT do
28//!
29//! - **No type-checking re-run**. The typechecker is the source of
30//!   truth for semantic decisions; the resolver only translates
31//!   names. If the typechecker passed, the resolver produces a
32//!   well-formed `ResolvedTopLevel` even when individual references
33//!   couldn't classify (those land in `Unresolved` passthroughs so
34//!   the surrounding tree still walks).
35//! - **No `Spanned::ty()` re-stamping**. Type stamps on the original
36//!   `Spanned<Expr>` nodes are already canonical (Phase B); the
37//!   resolver carries them across unchanged when relevant.
38//! - **No backend wiring**. Output is `Vec<ResolvedTopLevel>` —
39//!   producer only. Future PRs migrate each backend to consume it.
40//!
41//! ## Boundary with the typechecker
42//!
43//! Phase B made the typechecker's matcher operate on opaque IDs;
44//! Phase E generalises the same move to the AST itself. The
45//! resolver re-uses the symbol-table resolution rules the
46//! typechecker already exposes (qualified-then-bare lookups,
47//! built-in namespace recognition, builtin ctor distinction) so
48//! the two layers stay in lock-step.
49
50use std::sync::Arc;
51
52use crate::ast::{Expr, FnBody, FnDef, MatchArm, Pattern, Spanned, Stmt, TopLevel};
53use crate::ir::calls::{expr_to_dotted_name, is_builtin_namespace};
54use crate::ir::hir::{
55    BuiltinCtor, BuiltinIntrinsic, ResolvedCallee, ResolvedCtor, ResolvedExpr, ResolvedFnBody,
56    ResolvedFnDef, ResolvedMatchArm, ResolvedPattern, ResolvedStmt, ResolvedStrPart,
57    ResolvedTopLevel,
58};
59use crate::ir::identity::{FnId, FnKey, TypeId, TypeKey};
60use crate::ir::symbol_table::SymbolTable;
61
62/// Resolver state. Carries the symbol table the pass resolves
63/// against and the module prefix the current item lives in. The
64/// prefix matters for entry items that declare `module Main` — the
65/// symbol table stores them under `FnKey::entry` / `TypeKey::entry`,
66/// but the source-level references inside their bodies still use
67/// the interior name, so the resolver needs both candidates to
68/// match Phase B's canonical resolution rules.
69pub struct ResolveCtx<'a> {
70    pub symbols: &'a SymbolTable,
71    pub current_module: Option<String>,
72}
73
74impl<'a> ResolveCtx<'a> {
75    pub fn new(symbols: &'a SymbolTable) -> Self {
76        Self {
77            symbols,
78            current_module: None,
79        }
80    }
81
82    /// Resolve a fn reference to a `FnId`. Tries (in order):
83    ///   1. Qualified: `Module.fn` → `FnKey::in_module(Module, fn)`.
84    ///   2. Current module: `fn` → `FnKey::in_module(current, fn)`.
85    ///   3. Current-as-entry: when the qualified prefix matches the
86    ///      current module, also probe `FnKey::entry(fn)` (entry
87    ///      items live under entry scope in the symbol table even
88    ///      when they declare `module X`).
89    ///   4. Entry: `fn` → `FnKey::entry(fn)`.
90    pub fn resolve_fn_id(&self, name: &str) -> Option<FnId> {
91        if let Some((prefix, n)) = name.rsplit_once('.') {
92            if let Some(id) = self.symbols.fn_id_of(&FnKey::in_module(prefix, n)) {
93                return Some(id);
94            }
95            if self.current_module.as_deref() == Some(prefix)
96                && let Some(id) = self.symbols.fn_id_of(&FnKey::entry(n))
97            {
98                return Some(id);
99            }
100        }
101        if let Some(prefix) = self.current_module.as_deref()
102            && let Some(id) = self.symbols.fn_id_of(&FnKey::in_module(prefix, name))
103        {
104            return Some(id);
105        }
106        self.symbols.fn_id_of(&FnKey::entry(name))
107    }
108
109    /// Type-side equivalent of [`Self::resolve_fn_id`].
110    ///
111    /// Resolution order (first match wins):
112    /// 1. `prefix.name` matches `TypeKey::in_module(prefix, name)`.
113    /// 2. `prefix.name` with `prefix == current_module` matches the
114    ///    entry-scope `TypeKey::entry(name)` (Aver lets a
115    ///    self-referencing module spell its own type with the
116    ///    qualified form).
117    /// 3. Bare `name` in `current_module` (`TypeKey::in_module(current, name)`).
118    /// 4. Bare `name` in entry scope (`TypeKey::entry(name)`).
119    /// 5. Bare `name` searched across every scope via
120    ///    [`crate::ir::SymbolTable::type_id_by_bare_name`] — handles
121    ///    the cross-module case where module `A` references type
122    ///    `Val` declared in module `B` without qualification. Returns
123    ///    `None` when ambiguous (caller must qualify).
124    pub fn resolve_type_id(&self, name: &str) -> Option<TypeId> {
125        if let Some((prefix, n)) = name.rsplit_once('.') {
126            if let Some(id) = self.symbols.type_id_of(&TypeKey::in_module(prefix, n)) {
127                return Some(id);
128            }
129            if self.current_module.as_deref() == Some(prefix)
130                && let Some(id) = self.symbols.type_id_of(&TypeKey::entry(n))
131            {
132                return Some(id);
133            }
134        }
135        if let Some(prefix) = self.current_module.as_deref()
136            && let Some(id) = self.symbols.type_id_of(&TypeKey::in_module(prefix, name))
137        {
138            return Some(id);
139        }
140        if let Some(id) = self.symbols.type_id_of(&TypeKey::entry(name)) {
141            return Some(id);
142        }
143        // Cross-module bare-name fallback. Phase-E ctor resolution
144        // for `Val.ValOk(x)` written from a module that doesn't host
145        // `Val` (e.g. `Domain.Eval.Core` referencing
146        // `Domain.Value.Val`). `type_id_by_bare_name` returns `None`
147        // on ambiguity, so two scopes that share a type name still
148        // need a qualified reference.
149        self.symbols.type_id_by_bare_name(name)
150    }
151
152    /// Resolve a `"Type.Variant"` constructor reference to a
153    /// `(CtorId, TypeId, variant_name)` triple.
154    pub fn resolve_user_ctor(
155        &self,
156        dotted: &str,
157    ) -> Option<(crate::ir::identity::CtorId, TypeId, String)> {
158        let (type_name, variant) = dotted.rsplit_once('.')?;
159        let type_id = self.resolve_type_id(type_name)?;
160        let ctor_id = self.symbols.ctor_id_of(type_id, variant)?;
161        Some((ctor_id, type_id, variant.to_string()))
162    }
163}
164
165/// Resolve a whole program. Walks every top-level item once; items
166/// the resolver doesn't promote (verify blocks, decisions, type
167/// defs) land in [`ResolvedTopLevel::Passthrough`] so backends can
168/// iterate the same shape regardless.
169pub fn resolve_program(symbols: &SymbolTable, items: &[TopLevel]) -> Vec<ResolvedTopLevel> {
170    let module_name = items.iter().find_map(|i| match i {
171        TopLevel::Module(m) => Some(m.name.clone()),
172        _ => None,
173    });
174    let mut ctx = ResolveCtx::new(symbols);
175    ctx.current_module = module_name;
176    items.iter().map(|i| resolve_top_level(&ctx, i)).collect()
177}
178
179/// Resolve a single `FnDef` against an existing [`ResolveCtx`]. Mirror
180/// of [`resolve_stmt_external`] for callers that need to lift a free-
181/// standing fn def into resolved HIR — used by `CodegenContext` to
182/// populate `resolved_fn_defs` per-scope (entry + each dep) without
183/// re-running the whole program-level resolver.
184pub fn resolve_fn_def_external(ctx: &ResolveCtx<'_>, fd: &FnDef) -> Option<ResolvedFnDef> {
185    resolve_fn_def(ctx, fd)
186}
187
188pub fn resolve_top_level(ctx: &ResolveCtx<'_>, item: &TopLevel) -> ResolvedTopLevel {
189    match item {
190        TopLevel::Module(m) => ResolvedTopLevel::Module(m.clone()),
191        TopLevel::FnDef(fd) => match resolve_fn_def(ctx, fd) {
192            Some(rfd) => ResolvedTopLevel::FnDef(rfd),
193            // The fn's name didn't resolve through the symbol table.
194            // That only happens when the typechecker also rejected
195            // the program (or the program never went through
196            // build_signatures, e.g. a REPL-style item); either way
197            // we can't synthesise a `FnId`, so pass the original
198            // AST node through and let downstream consumers
199            // continue to handle it via the legacy `Expr` shape.
200            None => ResolvedTopLevel::Passthrough(item.clone()),
201        },
202        // Verify / Decision / TypeDef / top-level Stmt items pass
203        // through with their original AST representation. They're
204        // not on the runtime hot path; future PRs may promote them
205        // once the proof-export passes are ready.
206        TopLevel::Verify(_) | TopLevel::Decision(_) | TopLevel::Stmt(_) | TopLevel::TypeDef(_) => {
207            ResolvedTopLevel::Passthrough(item.clone())
208        }
209    }
210}
211
212fn resolve_fn_def(ctx: &ResolveCtx<'_>, fd: &FnDef) -> Option<ResolvedFnDef> {
213    let fn_id = ctx.resolve_fn_id(&fd.name)?;
214    let params: Vec<(String, crate::ast::Type)> = fd
215        .params
216        .iter()
217        .map(|(name, ann)| {
218            let ty = match crate::types::parse_type_str_strict(ann) {
219                Ok(t) => canonicalise_type(ctx, t),
220                Err(_) => crate::ast::Type::Invalid,
221            };
222            (name.clone(), ty)
223        })
224        .collect();
225    let return_type = match crate::types::parse_type_str_strict(&fd.return_type) {
226        Ok(t) => canonicalise_type(ctx, t),
227        Err(_) => crate::ast::Type::Invalid,
228    };
229    let body = resolve_fn_body(ctx, &fd.body);
230    Some(ResolvedFnDef {
231        fn_id,
232        name: fd.name.clone(),
233        line: fd.line,
234        params,
235        return_type,
236        effects: fd.effects.clone(),
237        desc: fd.desc.clone(),
238        body: Arc::new(body),
239        resolution: fd.resolution.clone(),
240    })
241}
242
243fn resolve_fn_body(ctx: &ResolveCtx<'_>, body: &FnBody) -> ResolvedFnBody {
244    match body {
245        FnBody::Block(stmts) => {
246            ResolvedFnBody::Block(stmts.iter().map(|s| resolve_stmt(ctx, s)).collect())
247        }
248    }
249}
250
251/// Resolve a single `Stmt` against a [`ResolveCtx`]. Exposed for
252/// callers that need to lift a free-standing statement into resolved
253/// HIR — top-level statements bypass the per-`FnDef` resolution path
254/// because they live in `TopLevel::Stmt` (which the program-level
255/// resolver leaves as `Passthrough` to avoid double-lifting them
256/// into a synthetic fn body).
257pub fn resolve_stmt_external(ctx: &ResolveCtx<'_>, stmt: &Stmt) -> ResolvedStmt {
258    resolve_stmt(ctx, stmt)
259}
260
261fn resolve_stmt(ctx: &ResolveCtx<'_>, stmt: &Stmt) -> ResolvedStmt {
262    match stmt {
263        Stmt::Binding(name, ann, expr) => {
264            let ty_ann = ann
265                .as_deref()
266                .map(|src| match crate::types::parse_type_str_strict(src) {
267                    Ok(t) => canonicalise_type(ctx, t),
268                    Err(_) => crate::ast::Type::Invalid,
269                });
270            ResolvedStmt::Binding {
271                name: name.clone(),
272                ty_ann,
273                value: resolve_spanned(ctx, expr),
274            }
275        }
276        Stmt::Expr(expr) => ResolvedStmt::Expr(resolve_spanned(ctx, expr)),
277    }
278}
279
280fn resolve_spanned(ctx: &ResolveCtx<'_>, expr: &Spanned<Expr>) -> Spanned<ResolvedExpr> {
281    let resolved = resolve_expr(ctx, expr);
282    let out = Spanned::new(resolved, expr.line);
283    if let Some(t) = expr.ty.get() {
284        let _ = out.ty.set(t.clone());
285    }
286    out
287}
288
289fn resolve_expr(ctx: &ResolveCtx<'_>, expr: &Spanned<Expr>) -> ResolvedExpr {
290    match &expr.node {
291        Expr::Literal(l) => ResolvedExpr::Literal(l.clone()),
292        Expr::Ident(name) => ResolvedExpr::Ident(name.clone()),
293        Expr::Resolved {
294            slot,
295            name,
296            last_use,
297        } => ResolvedExpr::Resolved {
298            slot: *slot,
299            name: name.clone(),
300            last_use: *last_use,
301        },
302        Expr::Attr(obj, field) => {
303            // Nullary constructor in value position — `Option.None`,
304            // `Color.Black`, etc. These only got recognized in call
305            // position (`Expr::FnCall`); in value position they arrived
306            // here as a bare `Attr(Ident(Type), Variant)` and stalled MIR
307            // lowering with `UnresolvedIdent`. A non-nullary ctor can't
308            // appear un-applied as a value (Aver has no first-class
309            // partial constructors), so an `Attr` whose `Type.Member`
310            // resolves to a ctor is necessarily a nullary reference —
311            // lower it to the zero-arg `Ctor` shape both walkers already
312            // handle (LOAD_CONST NONE / VARIANT_NEW with no fields).
313            if let Expr::Ident(ns) = &obj.node {
314                let qualified = format!("{ns}.{field}");
315                let ctor = classify_ctor(ctx, &qualified);
316                let nullary = matches!(
317                    ctor,
318                    ResolvedCtor::Builtin(BuiltinCtor::OptionNone) | ResolvedCtor::User { .. }
319                );
320                if nullary {
321                    return ResolvedExpr::Ctor(ctor, vec![]);
322                }
323            }
324            ResolvedExpr::Attr(Box::new(resolve_spanned(ctx, obj)), field.clone())
325        }
326        Expr::FnCall(callee, args) => {
327            let resolved_args: Vec<Spanned<ResolvedExpr>> =
328                args.iter().map(|a| resolve_spanned(ctx, a)).collect();
329            // Parser shape note: `Shape.Circle(1.0)` and
330            // `Result.Ok(42)` both parse as `FnCall(Attr(...), …)`
331            // — only the bare `Option.None` variant goes through
332            // `Expr::Constructor`. Recognise the ctor shape here so
333            // it lands as `ResolvedExpr::Ctor`, not `Call(Builtin,
334            // ...)` (which is for namespace methods like `Int.add`).
335            if let Some(dotted) = expr_to_dotted_name(&callee.node) {
336                if let Some(builtin) = parse_builtin_ctor(&dotted) {
337                    return ResolvedExpr::Ctor(ResolvedCtor::Builtin(builtin), resolved_args);
338                }
339                if let Some((ctor_id, type_id, variant_name)) = ctx.resolve_user_ctor(&dotted) {
340                    return ResolvedExpr::Ctor(
341                        ResolvedCtor::User {
342                            ctor_id,
343                            type_id,
344                            name: variant_name,
345                        },
346                        resolved_args,
347                    );
348                }
349            }
350            let resolved_callee = classify_callee(ctx, callee);
351            ResolvedExpr::Call(resolved_callee, resolved_args)
352        }
353        Expr::BinOp(op, l, r) => ResolvedExpr::BinOp(
354            *op,
355            Box::new(resolve_spanned(ctx, l)),
356            Box::new(resolve_spanned(ctx, r)),
357        ),
358        Expr::Neg(inner) => ResolvedExpr::Neg(Box::new(resolve_spanned(ctx, inner))),
359        Expr::Match { subject, arms } => ResolvedExpr::Match {
360            subject: Box::new(resolve_spanned(ctx, subject)),
361            arms: arms.iter().map(|a| resolve_match_arm(ctx, a)).collect(),
362        },
363        Expr::Constructor(name, arg) => {
364            let ctor = classify_ctor(ctx, name);
365            let args = match arg {
366                Some(a) => vec![resolve_spanned(ctx, a)],
367                None => vec![],
368            };
369            ResolvedExpr::Ctor(ctor, args)
370        }
371        Expr::ErrorProp(inner) => ResolvedExpr::ErrorProp(Box::new(resolve_spanned(ctx, inner))),
372        Expr::InterpolatedStr(parts) => ResolvedExpr::InterpolatedStr(
373            parts
374                .iter()
375                .map(|p| match p {
376                    crate::ast::StrPart::Literal(s) => ResolvedStrPart::Literal(s.clone()),
377                    crate::ast::StrPart::Parsed(inner) => {
378                        ResolvedStrPart::Parsed(Box::new(resolve_spanned(ctx, inner)))
379                    }
380                })
381                .collect(),
382        ),
383        Expr::List(items) => {
384            ResolvedExpr::List(items.iter().map(|i| resolve_spanned(ctx, i)).collect())
385        }
386        Expr::Tuple(items) => {
387            ResolvedExpr::Tuple(items.iter().map(|i| resolve_spanned(ctx, i)).collect())
388        }
389        Expr::MapLiteral(pairs) => ResolvedExpr::MapLiteral(
390            pairs
391                .iter()
392                .map(|(k, v)| (resolve_spanned(ctx, k), resolve_spanned(ctx, v)))
393                .collect(),
394        ),
395        Expr::RecordCreate { type_name, fields } => ResolvedExpr::RecordCreate {
396            type_id: ctx.resolve_type_id(type_name),
397            type_name: type_name.clone(),
398            fields: fields
399                .iter()
400                .map(|(n, e)| (n.clone(), resolve_spanned(ctx, e)))
401                .collect(),
402        },
403        Expr::RecordUpdate {
404            type_name,
405            base,
406            updates,
407        } => ResolvedExpr::RecordUpdate {
408            type_id: ctx.resolve_type_id(type_name),
409            type_name: type_name.clone(),
410            base: Box::new(resolve_spanned(ctx, base)),
411            updates: updates
412                .iter()
413                .map(|(n, e)| (n.clone(), resolve_spanned(ctx, e)))
414                .collect(),
415        },
416        Expr::TailCall(data) => {
417            let target = ctx.resolve_fn_id(&data.target);
418            let args = data.args.iter().map(|a| resolve_spanned(ctx, a)).collect();
419            match target {
420                Some(fn_id) => ResolvedExpr::TailCall {
421                    target: fn_id,
422                    args,
423                },
424                // TCO target couldn't resolve to a `FnId`. This is
425                // either pre-typecheck recovery state or a bug in
426                // the TCO pass; fall back to a `Call` with an
427                // `Unresolved` callee so the surrounding tree
428                // walks cleanly and the diagnostic stays
429                // attributable.
430                None => ResolvedExpr::Call(
431                    ResolvedCallee::Unresolved {
432                        callee: Box::new(Spanned::new(
433                            ResolvedExpr::Ident(data.target.clone()),
434                            expr.line,
435                        )),
436                    },
437                    args,
438                ),
439            }
440        }
441        Expr::IndependentProduct(items, unwrap) => ResolvedExpr::IndependentProduct(
442            items.iter().map(|i| resolve_spanned(ctx, i)).collect(),
443            *unwrap,
444        ),
445    }
446}
447
448fn resolve_match_arm(ctx: &ResolveCtx<'_>, arm: &MatchArm) -> ResolvedMatchArm {
449    let binding_slots = std::sync::OnceLock::new();
450    if let Some(slots) = arm.binding_slots.get() {
451        let _ = binding_slots.set(slots.clone());
452    }
453    ResolvedMatchArm {
454        pattern: resolve_pattern(ctx, &arm.pattern),
455        body: Box::new(resolve_spanned(ctx, &arm.body)),
456        binding_slots,
457    }
458}
459
460fn resolve_pattern(ctx: &ResolveCtx<'_>, pat: &Pattern) -> ResolvedPattern {
461    match pat {
462        Pattern::Wildcard => ResolvedPattern::Wildcard,
463        Pattern::Literal(l) => ResolvedPattern::Literal(l.clone()),
464        Pattern::Ident(name) => ResolvedPattern::Ident(name.clone()),
465        Pattern::EmptyList => ResolvedPattern::EmptyList,
466        Pattern::Cons(head, tail) => ResolvedPattern::Cons(head.clone(), tail.clone()),
467        Pattern::Tuple(items) => {
468            ResolvedPattern::Tuple(items.iter().map(|p| resolve_pattern(ctx, p)).collect())
469        }
470        Pattern::Constructor(name, bindings) => {
471            ResolvedPattern::Ctor(classify_ctor(ctx, name), bindings.clone())
472        }
473    }
474}
475
476/// Classify a call's callee expression. Branches:
477/// `Fn(FnId)` for user fns the symbol table knows about;
478/// `Builtin(name)` for namespace methods like `Int.add` /
479/// `Console.print`; `Intrinsic(_)` for the synthesised
480/// `__buf_*` / `__to_str` shapes emitted by `interp_lower` /
481/// `buffer_build`; `LocalSlot` for first-class fn values; the
482/// `Unresolved` passthrough for anything else (typechecker already
483/// reported it).
484fn classify_callee(ctx: &ResolveCtx<'_>, callee: &Spanned<Expr>) -> ResolvedCallee {
485    match &callee.node {
486        Expr::Resolved {
487            slot,
488            name,
489            last_use,
490        } => ResolvedCallee::LocalSlot {
491            slot: *slot,
492            name: name.clone(),
493            last_use: *last_use,
494        },
495        Expr::Ident(name) => {
496            if let Some(intrinsic) = BuiltinIntrinsic::from_name(name) {
497                return ResolvedCallee::Intrinsic(intrinsic);
498            }
499            // Some synthesised / pre-parsed shapes pack a fully
500            // qualified namespace method into a single `Ident`
501            // (`Ident("List.len")` rather than the parser's regular
502            // `Attr(Ident("List"), "len")`). Recognise the builtin
503            // namespace prefix so the resolver classifies them as
504            // `Builtin` rather than falling to `Unresolved` — keeps
505            // the source-shape classifier menu (`is_builtin_namespace`)
506            // honoured for both AST shapes.
507            if let Some((head, _)) = name.split_once('.')
508                && is_builtin_namespace(head)
509            {
510                return ResolvedCallee::Builtin(name.clone());
511            }
512            match ctx.resolve_fn_id(name) {
513                Some(id) => ResolvedCallee::Fn(id),
514                None => ResolvedCallee::Unresolved {
515                    callee: Box::new(resolve_spanned(ctx, callee)),
516                },
517            }
518        }
519        Expr::Attr(_, _) => {
520            let Some(dotted) = expr_to_dotted_name(&callee.node) else {
521                return ResolvedCallee::Unresolved {
522                    callee: Box::new(resolve_spanned(ctx, callee)),
523                };
524            };
525            // Try user-fn first — module-scoped calls like
526            // `Pricing.Discount.percent` route here.
527            if let Some(id) = ctx.resolve_fn_id(&dotted) {
528                return ResolvedCallee::Fn(id);
529            }
530            // Builtin namespace method (`Int.add`, `Console.print`).
531            // Use the existing classifier knowledge.
532            if let Some((head, _rest)) = dotted.split_once('.')
533                && is_builtin_namespace(head)
534            {
535                return ResolvedCallee::Builtin(dotted);
536            }
537            ResolvedCallee::Unresolved {
538                callee: Box::new(resolve_spanned(ctx, callee)),
539            }
540        }
541        _ => ResolvedCallee::Unresolved {
542            callee: Box::new(resolve_spanned(ctx, callee)),
543        },
544    }
545}
546
547/// Classify a `"Type.Variant"` (or built-in shortcut) constructor
548/// reference. Builtins (`Result.Ok`, `Result.Err`, `Option.Some`,
549/// `Option.None`) get the [`BuiltinCtor`] kind; user variants get
550/// `(CtorId, owning TypeId, variant name)`; anything else is an
551/// `Unresolved` passthrough.
552fn classify_ctor(ctx: &ResolveCtx<'_>, name: &str) -> ResolvedCtor {
553    if let Some(builtin) = parse_builtin_ctor(name) {
554        return ResolvedCtor::Builtin(builtin);
555    }
556    if let Some((ctor_id, type_id, variant_name)) = ctx.resolve_user_ctor(name) {
557        return ResolvedCtor::User {
558            ctor_id,
559            type_id,
560            name: variant_name,
561        };
562    }
563    ResolvedCtor::Unresolved {
564        name: name.to_string(),
565    }
566}
567
568fn parse_builtin_ctor(name: &str) -> Option<BuiltinCtor> {
569    match name {
570        "Result.Ok" => Some(BuiltinCtor::ResultOk),
571        "Result.Err" => Some(BuiltinCtor::ResultErr),
572        "Option.Some" => Some(BuiltinCtor::OptionSome),
573        // Both spellings — parser produces `Option.None` for the
574        // qualified form and just `None` when the expression
575        // appeared as a constructor literal without the qualifier.
576        "Option.None" | "None" => Some(BuiltinCtor::OptionNone),
577        _ => None,
578    }
579}
580
581/// Canonicalise a parsed `Type` against the resolver's symbol table.
582/// Sets `Type::Named { id: Some(_) }` when the name resolves; leaves
583/// `id: None` otherwise (builtin records, unresolved typo, …).
584/// Recurses through compound shapes so `Result<A.Shape, String>`
585/// gets the inner `A.Shape` resolved too.
586fn canonicalise_type(ctx: &ResolveCtx<'_>, ty: crate::ast::Type) -> crate::ast::Type {
587    use crate::ast::Type;
588    match ty {
589        // Peer review round 7: `Some(id)` is sacred. The typechecker
590        // already stamped real identities; never overwrite.
591        Type::Named {
592            id: Some(existing),
593            name,
594        } => Type::Named {
595            id: Some(existing),
596            name,
597        },
598        Type::Named { id: None, name } => match ctx.resolve_type_id(&name) {
599            Some(id) => Type::Named { id: Some(id), name },
600            None => Type::Named { id: None, name },
601        },
602        Type::List(inner) => Type::List(Box::new(canonicalise_type(ctx, *inner))),
603        Type::Vector(inner) => Type::Vector(Box::new(canonicalise_type(ctx, *inner))),
604        Type::Option(inner) => Type::Option(Box::new(canonicalise_type(ctx, *inner))),
605        Type::Result(ok, err) => Type::Result(
606            Box::new(canonicalise_type(ctx, *ok)),
607            Box::new(canonicalise_type(ctx, *err)),
608        ),
609        Type::Map(k, v) => Type::Map(
610            Box::new(canonicalise_type(ctx, *k)),
611            Box::new(canonicalise_type(ctx, *v)),
612        ),
613        Type::Tuple(items) => Type::Tuple(
614            items
615                .into_iter()
616                .map(|t| canonicalise_type(ctx, t))
617                .collect(),
618        ),
619        Type::Fn(params, ret, effects) => Type::Fn(
620            params
621                .into_iter()
622                .map(|t| canonicalise_type(ctx, t))
623                .collect(),
624            Box::new(canonicalise_type(ctx, *ret)),
625            effects,
626        ),
627        other => other,
628    }
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634    use crate::ast::{BinOp, Literal};
635    use crate::source::parse_source;
636    use crate::tco;
637
638    fn build(src: &str) -> (SymbolTable, Vec<TopLevel>) {
639        let mut items = parse_source(src).expect("parse failed");
640        tco::transform_program(&mut items);
641        let symbols = SymbolTable::build(&items, &[]);
642        (symbols, items)
643    }
644
645    fn first_fn(resolved: &[ResolvedTopLevel]) -> &ResolvedFnDef {
646        resolved
647            .iter()
648            .find_map(|t| match t {
649                ResolvedTopLevel::FnDef(f) => Some(f),
650                _ => None,
651            })
652            .expect("expected at least one resolved fn def")
653    }
654
655    #[test]
656    fn resolves_user_fn_call_to_fn_id() {
657        let (symbols, items) = build(
658            r#"
659fn main() -> Int
660    helper(1)
661
662fn helper(n: Int) -> Int
663    n + 1
664"#,
665        );
666        let resolved = resolve_program(&symbols, &items);
667        let main_fn = resolved
668            .iter()
669            .find_map(|t| match t {
670                ResolvedTopLevel::FnDef(f) if f.name == "main" => Some(f),
671                _ => None,
672            })
673            .expect("main");
674        let stmts = main_fn.body.stmts();
675        let ResolvedStmt::Expr(call) = &stmts[0] else {
676            panic!("expected tail Expr stmt, got {:?}", stmts[0]);
677        };
678        match &call.node {
679            ResolvedExpr::Call(ResolvedCallee::Fn(_), args) => {
680                assert_eq!(args.len(), 1);
681            }
682            other => panic!("expected Call(Fn, _), got {:?}", other),
683        }
684    }
685
686    #[test]
687    fn resolves_builtin_namespace_method_call() {
688        let (symbols, items) = build(
689            r#"
690fn add() -> Int
691    Int.abs(-3)
692"#,
693        );
694        let resolved = resolve_program(&symbols, &items);
695        let f = first_fn(&resolved);
696        let ResolvedStmt::Expr(call) = &f.body.stmts()[0] else {
697            panic!("expected tail Expr");
698        };
699        match &call.node {
700            ResolvedExpr::Call(ResolvedCallee::Builtin(name), _) => {
701                assert_eq!(name, "Int.abs");
702            }
703            other => panic!("expected Call(Builtin, _), got {:?}", other),
704        }
705    }
706
707    #[test]
708    fn resolves_user_ctor_to_ctor_id_and_owning_type() {
709        let (symbols, items) = build(
710            r#"
711type Shape
712    Circle(Float)
713    Square(Float)
714
715fn make() -> Shape
716    Shape.Circle(1.0)
717"#,
718        );
719        let resolved = resolve_program(&symbols, &items);
720        let f = first_fn(&resolved);
721        let ResolvedStmt::Expr(call) = &f.body.stmts()[0] else {
722            panic!("expected tail Expr");
723        };
724        match &call.node {
725            ResolvedExpr::Ctor(ResolvedCtor::User { type_id, name, .. }, args) => {
726                assert_eq!(name, "Circle");
727                assert!(*type_id != crate::ir::identity::TypeId(u32::MAX));
728                assert_eq!(args.len(), 1);
729            }
730            other => panic!("expected Ctor(User, _), got {:?}", other),
731        }
732    }
733
734    #[test]
735    fn resolves_builtin_ctor_result_ok() {
736        let (symbols, items) = build(
737            r#"
738fn make() -> Result<Int, String>
739    Result.Ok(42)
740"#,
741        );
742        let resolved = resolve_program(&symbols, &items);
743        let f = first_fn(&resolved);
744        let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
745            panic!("expected tail Expr");
746        };
747        match &expr.node {
748            ResolvedExpr::Ctor(ResolvedCtor::Builtin(BuiltinCtor::ResultOk), args) => {
749                assert_eq!(args.len(), 1);
750            }
751            other => panic!("expected Builtin(ResultOk), got {:?}", other),
752        }
753    }
754
755    #[test]
756    fn resolves_record_create_to_type_id() {
757        let (symbols, items) = build(
758            r#"
759record Point
760    x: Int
761    y: Int
762
763fn origin() -> Point
764    Point(x = 0, y = 0)
765"#,
766        );
767        let resolved = resolve_program(&symbols, &items);
768        let f = first_fn(&resolved);
769        let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
770            panic!("expected tail Expr");
771        };
772        match &expr.node {
773            ResolvedExpr::RecordCreate {
774                type_id, type_name, ..
775            } => {
776                assert!(type_id.is_some(), "Point should resolve to a TypeId");
777                assert_eq!(type_name, "Point");
778            }
779            other => panic!("expected RecordCreate, got {:?}", other),
780        }
781    }
782
783    #[test]
784    fn resolves_tail_call_target_to_fn_id() {
785        let (symbols, items) = build(
786            r#"
787fn count(n: Int, acc: Int) -> Int
788    match n
789        0 -> acc
790        _ -> count(n - 1, acc + 1)
791"#,
792        );
793        let resolved = resolve_program(&symbols, &items);
794        let f = first_fn(&resolved);
795        // Tail-call should be in the recursive arm. Walk the match.
796        let ResolvedStmt::Expr(top) = &f.body.stmts()[0] else {
797            panic!("expected tail Expr");
798        };
799        let ResolvedExpr::Match { arms, .. } = &top.node else {
800            panic!("expected match");
801        };
802        // Recursive arm is the second one.
803        let recursive_body = &arms[1].body.node;
804        match recursive_body {
805            ResolvedExpr::TailCall { target, args } => {
806                assert_eq!(args.len(), 2);
807                // FnId resolved against the symbol table — value
808                // depends on build order but it must be present.
809                let _ = target;
810            }
811            other => panic!("expected TailCall, got {:?}", other),
812        }
813    }
814
815    #[test]
816    fn resolves_binding_annotation_to_canonicalised_type() {
817        let (symbols, items) = build(
818            r#"
819fn pair() -> Int
820    x: Int = 5
821    x
822"#,
823        );
824        let resolved = resolve_program(&symbols, &items);
825        let f = first_fn(&resolved);
826        let stmts = f.body.stmts();
827        let ResolvedStmt::Binding { name, ty_ann, .. } = &stmts[0] else {
828            panic!("expected Binding stmt, got {:?}", stmts[0]);
829        };
830        assert_eq!(name, "x");
831        assert_eq!(ty_ann.as_ref(), Some(&crate::ast::Type::Int));
832    }
833
834    #[test]
835    fn binop_passes_through_structurally() {
836        let (symbols, items) = build(
837            r#"
838fn add() -> Int
839    1 + 2
840"#,
841        );
842        let resolved = resolve_program(&symbols, &items);
843        let f = first_fn(&resolved);
844        let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
845            panic!("tail expected");
846        };
847        match &expr.node {
848            ResolvedExpr::BinOp(BinOp::Add, l, r) => {
849                assert!(matches!(l.node, ResolvedExpr::Literal(Literal::Int(1))));
850                assert!(matches!(r.node, ResolvedExpr::Literal(Literal::Int(2))));
851            }
852            other => panic!("expected BinOp(Add, _, _), got {:?}", other),
853        }
854    }
855
856    #[test]
857    fn passthrough_for_verify_decision_typedef() {
858        let (symbols, items) = build(
859            r#"
860type Tag
861    On
862    Off
863
864verify alwaysTrue
865    1 => 1
866
867fn alwaysTrue() -> Int
868    1
869"#,
870        );
871        let resolved = resolve_program(&symbols, &items);
872        // TypeDef + Verify pass through; FnDef promotes.
873        let mut saw_passthrough = false;
874        let mut saw_fn = false;
875        for item in &resolved {
876            match item {
877                ResolvedTopLevel::Passthrough(_) => saw_passthrough = true,
878                ResolvedTopLevel::FnDef(_) => saw_fn = true,
879                _ => {}
880            }
881        }
882        assert!(saw_passthrough, "expected at least one passthrough item");
883        assert!(saw_fn, "expected the FnDef to be promoted");
884    }
885
886    #[test]
887    fn ty_stamps_preserved_on_resolved_spans() {
888        // After typechecker has stamped a `Spanned::ty()`, the
889        // resolved span must carry the same stamp. Phase B-era
890        // backends that read `expr.ty()` on the resolved AST need
891        // this invariant.
892        use crate::ast::{Expr as AstExpr, Spanned as AstSpanned};
893        let lit = AstSpanned::new(AstExpr::Literal(Literal::Int(7)), 1);
894        let _ = lit.ty.set(crate::ast::Type::Int);
895        // Build a one-item dummy symbol table — irrelevant for
896        // literal stamp propagation, just need a context.
897        let symbols = SymbolTable::default();
898        let ctx = ResolveCtx::new(&symbols);
899        let resolved = resolve_spanned(&ctx, &lit);
900        assert_eq!(resolved.ty.get(), Some(&crate::ast::Type::Int));
901    }
902}