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            ResolvedExpr::Attr(Box::new(resolve_spanned(ctx, obj)), field.clone())
304        }
305        Expr::FnCall(callee, args) => {
306            let resolved_args: Vec<Spanned<ResolvedExpr>> =
307                args.iter().map(|a| resolve_spanned(ctx, a)).collect();
308            // Parser shape note: `Shape.Circle(1.0)` and
309            // `Result.Ok(42)` both parse as `FnCall(Attr(...), …)`
310            // — only the bare `Option.None` variant goes through
311            // `Expr::Constructor`. Recognise the ctor shape here so
312            // it lands as `ResolvedExpr::Ctor`, not `Call(Builtin,
313            // ...)` (which is for namespace methods like `Int.add`).
314            if let Some(dotted) = expr_to_dotted_name(&callee.node) {
315                if let Some(builtin) = parse_builtin_ctor(&dotted) {
316                    return ResolvedExpr::Ctor(ResolvedCtor::Builtin(builtin), resolved_args);
317                }
318                if let Some((ctor_id, type_id, variant_name)) = ctx.resolve_user_ctor(&dotted) {
319                    return ResolvedExpr::Ctor(
320                        ResolvedCtor::User {
321                            ctor_id,
322                            type_id,
323                            name: variant_name,
324                        },
325                        resolved_args,
326                    );
327                }
328            }
329            let resolved_callee = classify_callee(ctx, callee);
330            ResolvedExpr::Call(resolved_callee, resolved_args)
331        }
332        Expr::BinOp(op, l, r) => ResolvedExpr::BinOp(
333            *op,
334            Box::new(resolve_spanned(ctx, l)),
335            Box::new(resolve_spanned(ctx, r)),
336        ),
337        Expr::Neg(inner) => ResolvedExpr::Neg(Box::new(resolve_spanned(ctx, inner))),
338        Expr::Match { subject, arms } => ResolvedExpr::Match {
339            subject: Box::new(resolve_spanned(ctx, subject)),
340            arms: arms.iter().map(|a| resolve_match_arm(ctx, a)).collect(),
341        },
342        Expr::Constructor(name, arg) => {
343            let ctor = classify_ctor(ctx, name);
344            let args = match arg {
345                Some(a) => vec![resolve_spanned(ctx, a)],
346                None => vec![],
347            };
348            ResolvedExpr::Ctor(ctor, args)
349        }
350        Expr::ErrorProp(inner) => ResolvedExpr::ErrorProp(Box::new(resolve_spanned(ctx, inner))),
351        Expr::InterpolatedStr(parts) => ResolvedExpr::InterpolatedStr(
352            parts
353                .iter()
354                .map(|p| match p {
355                    crate::ast::StrPart::Literal(s) => ResolvedStrPart::Literal(s.clone()),
356                    crate::ast::StrPart::Parsed(inner) => {
357                        ResolvedStrPart::Parsed(Box::new(resolve_spanned(ctx, inner)))
358                    }
359                })
360                .collect(),
361        ),
362        Expr::List(items) => {
363            ResolvedExpr::List(items.iter().map(|i| resolve_spanned(ctx, i)).collect())
364        }
365        Expr::Tuple(items) => {
366            ResolvedExpr::Tuple(items.iter().map(|i| resolve_spanned(ctx, i)).collect())
367        }
368        Expr::MapLiteral(pairs) => ResolvedExpr::MapLiteral(
369            pairs
370                .iter()
371                .map(|(k, v)| (resolve_spanned(ctx, k), resolve_spanned(ctx, v)))
372                .collect(),
373        ),
374        Expr::RecordCreate { type_name, fields } => ResolvedExpr::RecordCreate {
375            type_id: ctx.resolve_type_id(type_name),
376            type_name: type_name.clone(),
377            fields: fields
378                .iter()
379                .map(|(n, e)| (n.clone(), resolve_spanned(ctx, e)))
380                .collect(),
381        },
382        Expr::RecordUpdate {
383            type_name,
384            base,
385            updates,
386        } => ResolvedExpr::RecordUpdate {
387            type_id: ctx.resolve_type_id(type_name),
388            type_name: type_name.clone(),
389            base: Box::new(resolve_spanned(ctx, base)),
390            updates: updates
391                .iter()
392                .map(|(n, e)| (n.clone(), resolve_spanned(ctx, e)))
393                .collect(),
394        },
395        Expr::TailCall(data) => {
396            let target = ctx.resolve_fn_id(&data.target);
397            let args = data.args.iter().map(|a| resolve_spanned(ctx, a)).collect();
398            match target {
399                Some(fn_id) => ResolvedExpr::TailCall {
400                    target: fn_id,
401                    args,
402                },
403                // TCO target couldn't resolve to a `FnId`. This is
404                // either pre-typecheck recovery state or a bug in
405                // the TCO pass; fall back to a `Call` with an
406                // `Unresolved` callee so the surrounding tree
407                // walks cleanly and the diagnostic stays
408                // attributable.
409                None => ResolvedExpr::Call(
410                    ResolvedCallee::Unresolved {
411                        callee: Box::new(Spanned::new(
412                            ResolvedExpr::Ident(data.target.clone()),
413                            expr.line,
414                        )),
415                    },
416                    args,
417                ),
418            }
419        }
420        Expr::IndependentProduct(items, unwrap) => ResolvedExpr::IndependentProduct(
421            items.iter().map(|i| resolve_spanned(ctx, i)).collect(),
422            *unwrap,
423        ),
424    }
425}
426
427fn resolve_match_arm(ctx: &ResolveCtx<'_>, arm: &MatchArm) -> ResolvedMatchArm {
428    let binding_slots = std::sync::OnceLock::new();
429    if let Some(slots) = arm.binding_slots.get() {
430        let _ = binding_slots.set(slots.clone());
431    }
432    ResolvedMatchArm {
433        pattern: resolve_pattern(ctx, &arm.pattern),
434        body: Box::new(resolve_spanned(ctx, &arm.body)),
435        binding_slots,
436    }
437}
438
439fn resolve_pattern(ctx: &ResolveCtx<'_>, pat: &Pattern) -> ResolvedPattern {
440    match pat {
441        Pattern::Wildcard => ResolvedPattern::Wildcard,
442        Pattern::Literal(l) => ResolvedPattern::Literal(l.clone()),
443        Pattern::Ident(name) => ResolvedPattern::Ident(name.clone()),
444        Pattern::EmptyList => ResolvedPattern::EmptyList,
445        Pattern::Cons(head, tail) => ResolvedPattern::Cons(head.clone(), tail.clone()),
446        Pattern::Tuple(items) => {
447            ResolvedPattern::Tuple(items.iter().map(|p| resolve_pattern(ctx, p)).collect())
448        }
449        Pattern::Constructor(name, bindings) => {
450            ResolvedPattern::Ctor(classify_ctor(ctx, name), bindings.clone())
451        }
452    }
453}
454
455/// Classify a call's callee expression. Branches:
456/// `Fn(FnId)` for user fns the symbol table knows about;
457/// `Builtin(name)` for namespace methods like `Int.add` /
458/// `Console.print`; `Intrinsic(_)` for the synthesised
459/// `__buf_*` / `__to_str` shapes emitted by `interp_lower` /
460/// `buffer_build`; `LocalSlot` for first-class fn values; the
461/// `Unresolved` passthrough for anything else (typechecker already
462/// reported it).
463fn classify_callee(ctx: &ResolveCtx<'_>, callee: &Spanned<Expr>) -> ResolvedCallee {
464    match &callee.node {
465        Expr::Resolved {
466            slot,
467            name,
468            last_use,
469        } => ResolvedCallee::LocalSlot {
470            slot: *slot,
471            name: name.clone(),
472            last_use: *last_use,
473        },
474        Expr::Ident(name) => {
475            if let Some(intrinsic) = BuiltinIntrinsic::from_name(name) {
476                return ResolvedCallee::Intrinsic(intrinsic);
477            }
478            // Some synthesised / pre-parsed shapes pack a fully
479            // qualified namespace method into a single `Ident`
480            // (`Ident("List.len")` rather than the parser's regular
481            // `Attr(Ident("List"), "len")`). Recognise the builtin
482            // namespace prefix so the resolver classifies them as
483            // `Builtin` rather than falling to `Unresolved` — keeps
484            // the source-shape classifier menu (`is_builtin_namespace`)
485            // honoured for both AST shapes.
486            if let Some((head, _)) = name.split_once('.')
487                && is_builtin_namespace(head)
488            {
489                return ResolvedCallee::Builtin(name.clone());
490            }
491            match ctx.resolve_fn_id(name) {
492                Some(id) => ResolvedCallee::Fn(id),
493                None => ResolvedCallee::Unresolved {
494                    callee: Box::new(resolve_spanned(ctx, callee)),
495                },
496            }
497        }
498        Expr::Attr(_, _) => {
499            let Some(dotted) = expr_to_dotted_name(&callee.node) else {
500                return ResolvedCallee::Unresolved {
501                    callee: Box::new(resolve_spanned(ctx, callee)),
502                };
503            };
504            // Try user-fn first — module-scoped calls like
505            // `Pricing.Discount.percent` route here.
506            if let Some(id) = ctx.resolve_fn_id(&dotted) {
507                return ResolvedCallee::Fn(id);
508            }
509            // Builtin namespace method (`Int.add`, `Console.print`).
510            // Use the existing classifier knowledge.
511            if let Some((head, _rest)) = dotted.split_once('.')
512                && is_builtin_namespace(head)
513            {
514                return ResolvedCallee::Builtin(dotted);
515            }
516            ResolvedCallee::Unresolved {
517                callee: Box::new(resolve_spanned(ctx, callee)),
518            }
519        }
520        _ => ResolvedCallee::Unresolved {
521            callee: Box::new(resolve_spanned(ctx, callee)),
522        },
523    }
524}
525
526/// Classify a `"Type.Variant"` (or built-in shortcut) constructor
527/// reference. Builtins (`Result.Ok`, `Result.Err`, `Option.Some`,
528/// `Option.None`) get the [`BuiltinCtor`] kind; user variants get
529/// `(CtorId, owning TypeId, variant name)`; anything else is an
530/// `Unresolved` passthrough.
531fn classify_ctor(ctx: &ResolveCtx<'_>, name: &str) -> ResolvedCtor {
532    if let Some(builtin) = parse_builtin_ctor(name) {
533        return ResolvedCtor::Builtin(builtin);
534    }
535    if let Some((ctor_id, type_id, variant_name)) = ctx.resolve_user_ctor(name) {
536        return ResolvedCtor::User {
537            ctor_id,
538            type_id,
539            name: variant_name,
540        };
541    }
542    ResolvedCtor::Unresolved {
543        name: name.to_string(),
544    }
545}
546
547fn parse_builtin_ctor(name: &str) -> Option<BuiltinCtor> {
548    match name {
549        "Result.Ok" => Some(BuiltinCtor::ResultOk),
550        "Result.Err" => Some(BuiltinCtor::ResultErr),
551        "Option.Some" => Some(BuiltinCtor::OptionSome),
552        // Both spellings — parser produces `Option.None` for the
553        // qualified form and just `None` when the expression
554        // appeared as a constructor literal without the qualifier.
555        "Option.None" | "None" => Some(BuiltinCtor::OptionNone),
556        _ => None,
557    }
558}
559
560/// Canonicalise a parsed `Type` against the resolver's symbol table.
561/// Sets `Type::Named { id: Some(_) }` when the name resolves; leaves
562/// `id: None` otherwise (builtin records, unresolved typo, …).
563/// Recurses through compound shapes so `Result<A.Shape, String>`
564/// gets the inner `A.Shape` resolved too.
565fn canonicalise_type(ctx: &ResolveCtx<'_>, ty: crate::ast::Type) -> crate::ast::Type {
566    use crate::ast::Type;
567    match ty {
568        // Peer review round 7: `Some(id)` is sacred. The typechecker
569        // already stamped real identities; never overwrite.
570        Type::Named {
571            id: Some(existing),
572            name,
573        } => Type::Named {
574            id: Some(existing),
575            name,
576        },
577        Type::Named { id: None, name } => match ctx.resolve_type_id(&name) {
578            Some(id) => Type::Named { id: Some(id), name },
579            None => Type::Named { id: None, name },
580        },
581        Type::List(inner) => Type::List(Box::new(canonicalise_type(ctx, *inner))),
582        Type::Vector(inner) => Type::Vector(Box::new(canonicalise_type(ctx, *inner))),
583        Type::Option(inner) => Type::Option(Box::new(canonicalise_type(ctx, *inner))),
584        Type::Result(ok, err) => Type::Result(
585            Box::new(canonicalise_type(ctx, *ok)),
586            Box::new(canonicalise_type(ctx, *err)),
587        ),
588        Type::Map(k, v) => Type::Map(
589            Box::new(canonicalise_type(ctx, *k)),
590            Box::new(canonicalise_type(ctx, *v)),
591        ),
592        Type::Tuple(items) => Type::Tuple(
593            items
594                .into_iter()
595                .map(|t| canonicalise_type(ctx, t))
596                .collect(),
597        ),
598        Type::Fn(params, ret, effects) => Type::Fn(
599            params
600                .into_iter()
601                .map(|t| canonicalise_type(ctx, t))
602                .collect(),
603            Box::new(canonicalise_type(ctx, *ret)),
604            effects,
605        ),
606        other => other,
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613    use crate::ast::{BinOp, Literal};
614    use crate::source::parse_source;
615    use crate::tco;
616
617    fn build(src: &str) -> (SymbolTable, Vec<TopLevel>) {
618        let mut items = parse_source(src).expect("parse failed");
619        tco::transform_program(&mut items);
620        let symbols = SymbolTable::build(&items, &[]);
621        (symbols, items)
622    }
623
624    fn first_fn(resolved: &[ResolvedTopLevel]) -> &ResolvedFnDef {
625        resolved
626            .iter()
627            .find_map(|t| match t {
628                ResolvedTopLevel::FnDef(f) => Some(f),
629                _ => None,
630            })
631            .expect("expected at least one resolved fn def")
632    }
633
634    #[test]
635    fn resolves_user_fn_call_to_fn_id() {
636        let (symbols, items) = build(
637            r#"
638fn main() -> Int
639    helper(1)
640
641fn helper(n: Int) -> Int
642    n + 1
643"#,
644        );
645        let resolved = resolve_program(&symbols, &items);
646        let main_fn = resolved
647            .iter()
648            .find_map(|t| match t {
649                ResolvedTopLevel::FnDef(f) if f.name == "main" => Some(f),
650                _ => None,
651            })
652            .expect("main");
653        let stmts = main_fn.body.stmts();
654        let ResolvedStmt::Expr(call) = &stmts[0] else {
655            panic!("expected tail Expr stmt, got {:?}", stmts[0]);
656        };
657        match &call.node {
658            ResolvedExpr::Call(ResolvedCallee::Fn(_), args) => {
659                assert_eq!(args.len(), 1);
660            }
661            other => panic!("expected Call(Fn, _), got {:?}", other),
662        }
663    }
664
665    #[test]
666    fn resolves_builtin_namespace_method_call() {
667        let (symbols, items) = build(
668            r#"
669fn add() -> Int
670    Int.abs(-3)
671"#,
672        );
673        let resolved = resolve_program(&symbols, &items);
674        let f = first_fn(&resolved);
675        let ResolvedStmt::Expr(call) = &f.body.stmts()[0] else {
676            panic!("expected tail Expr");
677        };
678        match &call.node {
679            ResolvedExpr::Call(ResolvedCallee::Builtin(name), _) => {
680                assert_eq!(name, "Int.abs");
681            }
682            other => panic!("expected Call(Builtin, _), got {:?}", other),
683        }
684    }
685
686    #[test]
687    fn resolves_user_ctor_to_ctor_id_and_owning_type() {
688        let (symbols, items) = build(
689            r#"
690type Shape
691    Circle(Float)
692    Square(Float)
693
694fn make() -> Shape
695    Shape.Circle(1.0)
696"#,
697        );
698        let resolved = resolve_program(&symbols, &items);
699        let f = first_fn(&resolved);
700        let ResolvedStmt::Expr(call) = &f.body.stmts()[0] else {
701            panic!("expected tail Expr");
702        };
703        match &call.node {
704            ResolvedExpr::Ctor(ResolvedCtor::User { type_id, name, .. }, args) => {
705                assert_eq!(name, "Circle");
706                assert!(*type_id != crate::ir::identity::TypeId(u32::MAX));
707                assert_eq!(args.len(), 1);
708            }
709            other => panic!("expected Ctor(User, _), got {:?}", other),
710        }
711    }
712
713    #[test]
714    fn resolves_builtin_ctor_result_ok() {
715        let (symbols, items) = build(
716            r#"
717fn make() -> Result<Int, String>
718    Result.Ok(42)
719"#,
720        );
721        let resolved = resolve_program(&symbols, &items);
722        let f = first_fn(&resolved);
723        let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
724            panic!("expected tail Expr");
725        };
726        match &expr.node {
727            ResolvedExpr::Ctor(ResolvedCtor::Builtin(BuiltinCtor::ResultOk), args) => {
728                assert_eq!(args.len(), 1);
729            }
730            other => panic!("expected Builtin(ResultOk), got {:?}", other),
731        }
732    }
733
734    #[test]
735    fn resolves_record_create_to_type_id() {
736        let (symbols, items) = build(
737            r#"
738record Point
739    x: Int
740    y: Int
741
742fn origin() -> Point
743    Point(x = 0, y = 0)
744"#,
745        );
746        let resolved = resolve_program(&symbols, &items);
747        let f = first_fn(&resolved);
748        let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
749            panic!("expected tail Expr");
750        };
751        match &expr.node {
752            ResolvedExpr::RecordCreate {
753                type_id, type_name, ..
754            } => {
755                assert!(type_id.is_some(), "Point should resolve to a TypeId");
756                assert_eq!(type_name, "Point");
757            }
758            other => panic!("expected RecordCreate, got {:?}", other),
759        }
760    }
761
762    #[test]
763    fn resolves_tail_call_target_to_fn_id() {
764        let (symbols, items) = build(
765            r#"
766fn count(n: Int, acc: Int) -> Int
767    match n
768        0 -> acc
769        _ -> count(n - 1, acc + 1)
770"#,
771        );
772        let resolved = resolve_program(&symbols, &items);
773        let f = first_fn(&resolved);
774        // Tail-call should be in the recursive arm. Walk the match.
775        let ResolvedStmt::Expr(top) = &f.body.stmts()[0] else {
776            panic!("expected tail Expr");
777        };
778        let ResolvedExpr::Match { arms, .. } = &top.node else {
779            panic!("expected match");
780        };
781        // Recursive arm is the second one.
782        let recursive_body = &arms[1].body.node;
783        match recursive_body {
784            ResolvedExpr::TailCall { target, args } => {
785                assert_eq!(args.len(), 2);
786                // FnId resolved against the symbol table — value
787                // depends on build order but it must be present.
788                let _ = target;
789            }
790            other => panic!("expected TailCall, got {:?}", other),
791        }
792    }
793
794    #[test]
795    fn resolves_binding_annotation_to_canonicalised_type() {
796        let (symbols, items) = build(
797            r#"
798fn pair() -> Int
799    x: Int = 5
800    x
801"#,
802        );
803        let resolved = resolve_program(&symbols, &items);
804        let f = first_fn(&resolved);
805        let stmts = f.body.stmts();
806        let ResolvedStmt::Binding { name, ty_ann, .. } = &stmts[0] else {
807            panic!("expected Binding stmt, got {:?}", stmts[0]);
808        };
809        assert_eq!(name, "x");
810        assert_eq!(ty_ann.as_ref(), Some(&crate::ast::Type::Int));
811    }
812
813    #[test]
814    fn binop_passes_through_structurally() {
815        let (symbols, items) = build(
816            r#"
817fn add() -> Int
818    1 + 2
819"#,
820        );
821        let resolved = resolve_program(&symbols, &items);
822        let f = first_fn(&resolved);
823        let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
824            panic!("tail expected");
825        };
826        match &expr.node {
827            ResolvedExpr::BinOp(BinOp::Add, l, r) => {
828                assert!(matches!(l.node, ResolvedExpr::Literal(Literal::Int(1))));
829                assert!(matches!(r.node, ResolvedExpr::Literal(Literal::Int(2))));
830            }
831            other => panic!("expected BinOp(Add, _, _), got {:?}", other),
832        }
833    }
834
835    #[test]
836    fn passthrough_for_verify_decision_typedef() {
837        let (symbols, items) = build(
838            r#"
839type Tag
840    On
841    Off
842
843verify alwaysTrue
844    1 => 1
845
846fn alwaysTrue() -> Int
847    1
848"#,
849        );
850        let resolved = resolve_program(&symbols, &items);
851        // TypeDef + Verify pass through; FnDef promotes.
852        let mut saw_passthrough = false;
853        let mut saw_fn = false;
854        for item in &resolved {
855            match item {
856                ResolvedTopLevel::Passthrough(_) => saw_passthrough = true,
857                ResolvedTopLevel::FnDef(_) => saw_fn = true,
858                _ => {}
859            }
860        }
861        assert!(saw_passthrough, "expected at least one passthrough item");
862        assert!(saw_fn, "expected the FnDef to be promoted");
863    }
864
865    #[test]
866    fn ty_stamps_preserved_on_resolved_spans() {
867        // After typechecker has stamped a `Spanned::ty()`, the
868        // resolved span must carry the same stamp. Phase B-era
869        // backends that read `expr.ty()` on the resolved AST need
870        // this invariant.
871        use crate::ast::{Expr as AstExpr, Spanned as AstSpanned};
872        let lit = AstSpanned::new(AstExpr::Literal(Literal::Int(7)), 1);
873        let _ = lit.ty.set(crate::ast::Type::Int);
874        // Build a one-item dummy symbol table — irrelevant for
875        // literal stamp propagation, just need a context.
876        let symbols = SymbolTable::default();
877        let ctx = ResolveCtx::new(&symbols);
878        let resolved = resolve_spanned(&ctx, &lit);
879        assert_eq!(resolved.ty.get(), Some(&crate::ast::Type::Int));
880    }
881}