Skip to main content

aver/ir/hir/
mod.rs

1//! Resolved AST / High-level IR — typed-identity layer between
2//! [`crate::ast`] and the backends.
3//!
4//! ## Why this layer exists
5//!
6//! Pre-Phase-E backends consumed `crate::ast::Expr` directly. That
7//! shape carries source-level names everywhere: `Expr::FnCall(Ident
8//! "foo")` for a call, `Expr::Constructor("Result.Ok", arg)` for a
9//! variant, `Expr::RecordCreate { type_name: "Shape", … }` for a
10//! record. Every backend (VM compiler, Rust codegen, wasm-gc, Lean,
11//! Dafny, self-host) re-derived identity from those strings — six
12//! independent resolvers each guessing the same answer, drifting
13//! whenever a corner case touched only one of them. The proof IR
14//! layer (PRs #141–#146) moved the proof flow onto opaque `FnId` /
15//! `TypeId` / `CtorId`; Phase B (#148) did the same for the
16//! typechecker's internal `fn_sigs` + matcher. Phase E generalises
17//! the move to a shared resolved AST every backend reads.
18//!
19//! ## Contract
20//!
21//! [`ResolvedExpr`] is a mechanical translation of `Expr` — same
22//! shape, same operator precedence, same evaluation order. The only
23//! difference is that every name that referred to a declared symbol
24//! is replaced by its opaque ID:
25//!
26//! - `FnCall(Box<Spanned<Expr>>, …)` → [`ResolvedExpr::Call`] with
27//!   a [`ResolvedCallee`] discriminating user fn / builtin namespace
28//!   method / local lambda / ambient operator.
29//! - `Constructor(String, …)` → [`ResolvedExpr::Ctor`] with a
30//!   [`ResolvedCtor`] discriminating user `CtorId` / built-in
31//!   variant (`Result.Ok` / `Result.Err` / `Option.Some` /
32//!   `Option.None`).
33//! - `RecordCreate { type_name, fields }` → [`ResolvedExpr::RecordCreate`]
34//!   with a `TypeId` (always `Some` for user records — built-in
35//!   record types stay `Type::Named { id: None, … }` everywhere,
36//!   including here).
37//! - `RecordUpdate` → [`ResolvedExpr::RecordUpdate`] same.
38//! - `TailCall(TailCallData)` → [`ResolvedExpr::TailCall`] with a
39//!   `FnId` for the target.
40//! - `Pattern::Constructor(name, bindings)` → [`ResolvedPattern::Ctor`]
41//!   with a [`ResolvedCtor`].
42//! - `Stmt::Binding(name, Option<String>, expr)` →
43//!   [`ResolvedStmt::Binding`] with a resolved
44//!   [`crate::ast::Type`] annotation instead of a string.
45//!
46//! Anything that doesn't reference a declared symbol passes through
47//! structurally — literals, binops, neg, list / tuple / map
48//! literals, interpolation, independent products, error-prop, slot
49//! resolutions (`Expr::Resolved`), match arms (recursively
50//! resolved).
51//!
52//! ## What this layer does NOT do
53//!
54//! - **No semantic information added** beyond identity. Refinement
55//!   subtype decisions, recursion contracts, law theorems — all of
56//!   that stays in [`crate::ir::ProofIR`]. Resolved HIR is the
57//!   universal substrate, not a proof IR.
58//! - **No optimization**. The lowering is one-to-one; the same
59//!   number of expressions land on the other side.
60//! - **No effect propagation rewriting**. Effects stay on the
61//!   resolved fn signature exactly as the typechecker classified
62//!   them.
63//!
64//! ## What's NOT in this PR
65//!
66//! This module ships the shape only — definitions + smoke tests.
67//! Future PRs in the Phase E stack:
68//!
69//! - **PR 2** (`name_resolve` pass): builds [`ResolvedExpr`] from
70//!   `Expr` by walking the AST against a `SymbolTable` +
71//!   typechecker result.
72//! - **PR 3+** (one per backend): each backend migrates from
73//!   consuming `Expr` to [`ResolvedExpr`]. Old paths stay as
74//!   fallback during migration.
75//! - **PR N** (cleanup): drop pre-resolve consumers, remove the
76//!   bare-string lookups that the resolved layer subsumes (Phase D
77//!   in #147 terminology).
78
79use crate::ast::{AnnotBool, BinOp, FnResolution, Literal, Module, Spanned, Type};
80use crate::ir::identity::{CtorId, FnId, TypeId};
81
82pub mod classify;
83pub mod dump;
84pub mod resolve;
85pub use classify::{
86    ForwardSlot, ResolvedBodyBindingPlan, ResolvedBodyExprPlan, ResolvedBodyPlan,
87    ResolvedBoolSubjectPlan, ResolvedForwardCallPlan, ResolvedLeafOp, ResolvedThinBodyPlan,
88    ThinKind, call_plan_from_resolved_callee, classify_body_expr_plan_resolved,
89    classify_body_plan_resolved, classify_bool_match_shape_resolved,
90    classify_bool_subject_plan_resolved, classify_dispatch_pattern_resolved,
91    classify_dispatch_table_shape_resolved, classify_forward_call_resolved,
92    classify_leaf_op_resolved, classify_list_match_shape_resolved,
93    classify_match_dispatch_plan_resolved, classify_thin_fn_def_resolved, resolved_to_dotted,
94    semantic_constructor_from_resolved_ctor,
95};
96pub use dump::{dump_resolved_expr, dump_resolved_program};
97pub use resolve::{ResolveCtx, resolve_fn_def_external, resolve_program, resolve_top_level};
98
99/// Count every `ResolvedCallee::Unresolved` and
100/// `ResolvedCtor::Unresolved` reachable from `fd`'s body. The Phase E
101/// contract for `resolved_items` is **zero unresolved for well-typed
102/// programs** — non-zero means either the typechecker already
103/// rejected the program (resolver bailed to recovery) or there's a
104/// resolver gap. CI invariants compare this count against typecheck
105/// errors to catch silent regressions.
106pub fn count_unresolved_in_fn(fd: &ResolvedFnDef) -> usize {
107    let mut count = 0;
108    count_unresolved_in_body(&fd.body, &mut count);
109    count
110}
111
112fn count_unresolved_in_body(body: &ResolvedFnBody, out: &mut usize) {
113    match body {
114        ResolvedFnBody::Block(stmts) => {
115            for stmt in stmts {
116                count_unresolved_in_stmt(stmt, out);
117            }
118        }
119    }
120}
121
122fn count_unresolved_in_stmt(stmt: &ResolvedStmt, out: &mut usize) {
123    match stmt {
124        ResolvedStmt::Binding { value, .. } | ResolvedStmt::Expr(value) => {
125            count_unresolved_in_expr(&value.node, out)
126        }
127    }
128}
129
130fn count_unresolved_in_expr(expr: &ResolvedExpr, out: &mut usize) {
131    match expr {
132        ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. } => {}
133        ResolvedExpr::Attr(obj, _) => count_unresolved_in_expr(&obj.node, out),
134        ResolvedExpr::Call(callee, args) => {
135            if let ResolvedCallee::Unresolved { callee } = callee {
136                *out += 1;
137                count_unresolved_in_expr(&callee.node, out);
138            }
139            for a in args {
140                count_unresolved_in_expr(&a.node, out);
141            }
142        }
143        ResolvedExpr::BinOp(_, l, r) => {
144            count_unresolved_in_expr(&l.node, out);
145            count_unresolved_in_expr(&r.node, out);
146        }
147        ResolvedExpr::Neg(inner) | ResolvedExpr::ErrorProp(inner) => {
148            count_unresolved_in_expr(&inner.node, out)
149        }
150        ResolvedExpr::Match { subject, arms } => {
151            count_unresolved_in_expr(&subject.node, out);
152            for arm in arms {
153                count_unresolved_in_pattern(&arm.pattern, out);
154                count_unresolved_in_expr(&arm.body.node, out);
155            }
156        }
157        ResolvedExpr::Ctor(ctor, args) => {
158            if matches!(ctor, ResolvedCtor::Unresolved { .. }) {
159                *out += 1;
160            }
161            for a in args {
162                count_unresolved_in_expr(&a.node, out);
163            }
164        }
165        ResolvedExpr::InterpolatedStr(parts) => {
166            for p in parts {
167                if let ResolvedStrPart::Parsed(inner) = p {
168                    count_unresolved_in_expr(&inner.node, out);
169                }
170            }
171        }
172        ResolvedExpr::List(items)
173        | ResolvedExpr::Tuple(items)
174        | ResolvedExpr::IndependentProduct(items, _) => {
175            for i in items {
176                count_unresolved_in_expr(&i.node, out);
177            }
178        }
179        ResolvedExpr::MapLiteral(pairs) => {
180            for (k, v) in pairs {
181                count_unresolved_in_expr(&k.node, out);
182                count_unresolved_in_expr(&v.node, out);
183            }
184        }
185        ResolvedExpr::RecordCreate { fields, .. } => {
186            for (_, e) in fields {
187                count_unresolved_in_expr(&e.node, out);
188            }
189        }
190        ResolvedExpr::RecordUpdate { base, updates, .. } => {
191            count_unresolved_in_expr(&base.node, out);
192            for (_, e) in updates {
193                count_unresolved_in_expr(&e.node, out);
194            }
195        }
196        ResolvedExpr::TailCall { args, .. } => {
197            for a in args {
198                count_unresolved_in_expr(&a.node, out);
199            }
200        }
201    }
202}
203
204fn count_unresolved_in_pattern(pat: &ResolvedPattern, out: &mut usize) {
205    match pat {
206        ResolvedPattern::Wildcard
207        | ResolvedPattern::Literal(_)
208        | ResolvedPattern::Ident(_)
209        | ResolvedPattern::EmptyList
210        | ResolvedPattern::Cons(_, _) => {}
211        ResolvedPattern::Tuple(items) => {
212            for p in items {
213                count_unresolved_in_pattern(p, out);
214            }
215        }
216        ResolvedPattern::Ctor(ctor, _) => {
217            if matches!(ctor, ResolvedCtor::Unresolved { .. }) {
218                *out += 1;
219            }
220        }
221    }
222}
223
224/// Resolved expression — the mechanical mirror of [`crate::ast::Expr`].
225///
226/// Every variant that referenced a declared symbol by string in
227/// `Expr` now carries its opaque ID. Variants that didn't reference
228/// any declaration pass through unchanged in shape.
229#[derive(Debug, Clone, PartialEq)]
230pub enum ResolvedExpr {
231    /// Literal — untouched.
232    Literal(Literal),
233    /// Source-level identifier that the resolver pass could NOT
234    /// classify into a `Resolved` slot, a fn ref, or a constructor.
235    /// Typically a top-level / global binding name; backends look it
236    /// up in the post-resolve global table.
237    ///
238    /// After full Phase E + D, this variant should rarely survive —
239    /// every name has either a slot, an `FnId`, a `CtorId`, or
240    /// fails to type-check. Kept here as the safety hatch during
241    /// migration.
242    Ident(String),
243    /// Compiled local-slot reference. Identical to `Expr::Resolved`
244    /// — the slot resolver runs before name-resolve and its output
245    /// is already typed identity.
246    Resolved {
247        slot: u16,
248        name: String,
249        last_use: AnnotBool,
250    },
251    /// Field / namespace projection. The object is recursively
252    /// resolved; the field name stays as source text because record
253    /// fields are scoped under their owning `TypeId`, not in a
254    /// global namespace.
255    Attr(Box<Spanned<ResolvedExpr>>, String),
256    /// Function call — `Expr::FnCall(callee, args)` lifted into a
257    /// [`ResolvedCallee`].
258    Call(ResolvedCallee, Vec<Spanned<ResolvedExpr>>),
259    /// Binary operator.
260    BinOp(
261        BinOp,
262        Box<Spanned<ResolvedExpr>>,
263        Box<Spanned<ResolvedExpr>>,
264    ),
265    /// Unary minus.
266    Neg(Box<Spanned<ResolvedExpr>>),
267    /// `match subject { arm, … }`.
268    Match {
269        subject: Box<Spanned<ResolvedExpr>>,
270        arms: Vec<ResolvedMatchArm>,
271    },
272    /// Constructor call — `Result.Ok(v)`, `Shape.Circle(r)`, etc.
273    Ctor(ResolvedCtor, Vec<Spanned<ResolvedExpr>>),
274    /// Result-error propagation: `expr?`.
275    ErrorProp(Box<Spanned<ResolvedExpr>>),
276    /// Interpolated string `"a${x}b"`.
277    InterpolatedStr(Vec<ResolvedStrPart>),
278    /// `[a, b, c]` list literal.
279    List(Vec<Spanned<ResolvedExpr>>),
280    /// `(a, b, c)` tuple literal.
281    Tuple(Vec<Spanned<ResolvedExpr>>),
282    /// `{"a" => 1, "b" => 2}` map literal.
283    MapLiteral(Vec<(Spanned<ResolvedExpr>, Spanned<ResolvedExpr>)>),
284    /// `Shape(name = "...", count = 0)` record-create form. The
285    /// `type_id` is `Some` for user records resolved through the
286    /// symbol table, `None` for built-in record types (`HttpResponse`,
287    /// `Header`, `Tcp.Connection`, `Buffer`, …) which don't carry
288    /// `TypeId`s by design.
289    RecordCreate {
290        type_id: Option<TypeId>,
291        /// Source-level type name kept verbatim for diagnostics +
292        /// backend codegen mangling. Mirrors `Type::Named { name }`.
293        type_name: String,
294        fields: Vec<(String, Spanned<ResolvedExpr>)>,
295    },
296    /// `Shape.update(base, field = newVal, …)` record-update.
297    RecordUpdate {
298        type_id: Option<TypeId>,
299        type_name: String,
300        base: Box<Spanned<ResolvedExpr>>,
301        updates: Vec<(String, Spanned<ResolvedExpr>)>,
302    },
303    /// Tail-position call (SCC peer). `target` is the `FnId` the
304    /// TCO transform pass committed to.
305    TailCall {
306        target: FnId,
307        args: Vec<Spanned<ResolvedExpr>>,
308    },
309    /// Independent product `(a, b)!` or `(a, b)?!`. `unwrap` toggles
310    /// the `?!` form.
311    IndependentProduct(Vec<Spanned<ResolvedExpr>>, bool),
312}
313
314/// Callee classification for [`ResolvedExpr::Call`]. Replaces the
315/// pre-Phase-E `FnCall(Box<Spanned<Expr>>, …)` shape where the
316/// callee was an arbitrary `Expr` subtree that every backend re-
317/// parsed.
318#[derive(Debug, Clone, PartialEq)]
319pub enum ResolvedCallee {
320    /// User-defined fn (entry module or any dep). Resolved by name-
321    /// resolve against the program's `SymbolTable`.
322    Fn(FnId),
323    /// Built-in namespace method — `Int.add`, `Console.print`,
324    /// `Vector.fromList`, etc. These don't have `FnId`s in the
325    /// program symbol table; carrier is the canonical
326    /// `"Namespace.method"` string. The lookup table for these is
327    /// flat and global, so a string key is enough — and stable
328    /// across compiler versions.
329    Builtin(String),
330    /// Compiler-synthesised intrinsic emitted by the
331    /// `interp_lower` / `buffer_build` deforestation passes.
332    /// Source-illegal names (`__buf_*`, `__to_str`) that only ever
333    /// appear after lowering. Carrying them as a typed variant
334    /// instead of `Unresolved { Ident("__buf_*") }` keeps the
335    /// "well-typed → zero unresolved" invariant honest — see
336    /// [`BuiltinIntrinsic`] for the enumerated set and the
337    /// `name_resolve_invariant_zero_unresolved_*` tests in
338    /// [`crate::ir::pipeline`].
339    Intrinsic(BuiltinIntrinsic),
340    /// First-class fn value bound to a local slot (lambda / fn ref
341    /// passed as an argument). The slot resolver already mapped
342    /// the binding; we just thread the slot through.
343    LocalSlot {
344        slot: u16,
345        name: String,
346        last_use: AnnotBool,
347    },
348    /// Callee shape the resolver couldn't classify — typically a
349    /// type error the typechecker already reported. Backends bail
350    /// out cleanly when they see this; pass exists so resolve can
351    /// emit a placeholder instead of panicking.
352    Unresolved {
353        /// The source-faithful expression the resolver gave up on,
354        /// kept for diagnostics. Resolved recursively into the same
355        /// IR so the surrounding tree still walks cleanly.
356        callee: Box<Spanned<ResolvedExpr>>,
357    },
358}
359
360/// The enumerated set of compiler-synthesised call intrinsics. New
361/// variants are added only when a lowering pass introduces a new
362/// `Expr::Ident("__…")` shape — there is no user-source mapping.
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub enum BuiltinIntrinsic {
365    /// `__buf_new(<cap_hint>)` — allocate a fresh buffer slot.
366    BufNew,
367    /// `__buf_append(<buf>, <str>)` — concatenate a string fragment
368    /// onto the host-side buffer pool entry.
369    BufAppend,
370    /// `__buf_append_sep_unless_first(<buf>, <sep>)` — emit the
371    /// separator before every fragment except the first.
372    BufAppendSepUnlessFirst,
373    /// `__buf_finalize(<buf>)` — materialise the buffer pool entry
374    /// into an Aver `String` value and free the slot.
375    BufFinalize,
376    /// `__to_str(<value>)` — coerce any value to its display string
377    /// (used by interpolation lowering before `__buf_append`).
378    ToStr,
379}
380
381impl BuiltinIntrinsic {
382    /// Canonical source-level name. Used by diagnostic dumps and as
383    /// the bridge into the resolver / VM dispatch tables.
384    pub const fn name(self) -> &'static str {
385        match self {
386            Self::BufNew => "__buf_new",
387            Self::BufAppend => "__buf_append",
388            Self::BufAppendSepUnlessFirst => "__buf_append_sep_unless_first",
389            Self::BufFinalize => "__buf_finalize",
390            Self::ToStr => "__to_str",
391        }
392    }
393
394    /// Recognise a bare identifier as one of the known intrinsics.
395    /// Returns `None` for anything else — the resolver then falls
396    /// through to its regular fn / Unresolved classification.
397    pub fn from_name(name: &str) -> Option<Self> {
398        match name {
399            "__buf_new" => Some(Self::BufNew),
400            "__buf_append" => Some(Self::BufAppend),
401            "__buf_append_sep_unless_first" => Some(Self::BufAppendSepUnlessFirst),
402            "__buf_finalize" => Some(Self::BufFinalize),
403            "__to_str" => Some(Self::ToStr),
404            _ => None,
405        }
406    }
407}
408
409/// Constructor classification for [`ResolvedExpr::Ctor`] and
410/// [`ResolvedPattern::Ctor`].
411#[derive(Debug, Clone, PartialEq)]
412pub enum ResolvedCtor {
413    /// User-defined sum-type variant or record constructor.
414    User {
415        ctor_id: CtorId,
416        /// Owning type identity — kept on the callsite so backends
417        /// that pattern-match on (type, variant) don't need to walk
418        /// the symbol table on every emit.
419        type_id: TypeId,
420        /// Source name of the variant (`"Circle"`, `"Triangle"`,
421        /// the record name itself for product types). Kept for
422        /// diagnostic display + backend codegen mangling.
423        name: String,
424    },
425    /// `Result.Ok` / `Result.Err` / `Option.Some` / `Option.None`.
426    /// These are language-level rather than user-defined so they
427    /// don't carry `CtorId`s — discriminated by the variant.
428    Builtin(BuiltinCtor),
429    /// Constructor expression the resolver couldn't classify —
430    /// typechecker already reported the error. Kept as a
431    /// passthrough so the surrounding tree walks cleanly.
432    Unresolved {
433        /// Source name (`"Foo.Bar"`) preserved for diagnostics.
434        name: String,
435    },
436}
437
438/// Identity of the four built-in algebraic constructors. Mirror of
439/// the `Result` / `Option` types that the language exposes
440/// implicitly — these never get a user-program `CtorId` because
441/// they're not user-declared.
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum BuiltinCtor {
444    ResultOk,
445    ResultErr,
446    OptionSome,
447    OptionNone,
448}
449
450/// Interpolated string piece — mirror of [`crate::ast::StrPart`].
451#[derive(Debug, Clone, PartialEq)]
452pub enum ResolvedStrPart {
453    Literal(String),
454    Parsed(Box<Spanned<ResolvedExpr>>),
455}
456
457/// One arm of a `match` — mirror of [`crate::ast::MatchArm`] with
458/// the pattern lifted to [`ResolvedPattern`].
459#[derive(Debug)]
460pub struct ResolvedMatchArm {
461    pub pattern: ResolvedPattern,
462    pub body: Box<Spanned<ResolvedExpr>>,
463    /// Per-arm slot table — same role as
464    /// [`crate::ast::MatchArm::binding_slots`]. Populated by the
465    /// resolver pass; backends read it instead of re-doing the
466    /// name → slot lookup.
467    pub binding_slots: std::sync::OnceLock<Vec<u16>>,
468}
469
470impl Clone for ResolvedMatchArm {
471    fn clone(&self) -> Self {
472        let binding_slots = std::sync::OnceLock::new();
473        if let Some(v) = self.binding_slots.get() {
474            let _ = binding_slots.set(v.clone());
475        }
476        Self {
477            pattern: self.pattern.clone(),
478            body: self.body.clone(),
479            binding_slots,
480        }
481    }
482}
483
484impl PartialEq for ResolvedMatchArm {
485    fn eq(&self, other: &Self) -> bool {
486        // `binding_slots` is filled by a later pass and isn't part
487        // of structural identity — same rule [`MatchArm`] uses.
488        self.pattern == other.pattern && self.body == other.body
489    }
490}
491
492/// Pattern shape — mirror of [`crate::ast::Pattern`].
493#[derive(Debug, Clone, PartialEq)]
494pub enum ResolvedPattern {
495    Wildcard,
496    Literal(Literal),
497    Ident(String),
498    EmptyList,
499    Cons(String, String),
500    Tuple(Vec<ResolvedPattern>),
501    /// Constructor pattern — `Result.Ok(x)`, `Shape.Circle(r)`,
502    /// `Shape.Point`. Resolved to a [`ResolvedCtor`] +
503    /// pattern-binding names.
504    Ctor(ResolvedCtor, Vec<String>),
505}
506
507/// Statement form — mirror of [`crate::ast::Stmt`] with the
508/// optional type annotation lifted to [`Type`] (already canonicalised
509/// against the resolver's symbol table) instead of a source string.
510#[derive(Debug, Clone, PartialEq)]
511pub enum ResolvedStmt {
512    Binding {
513        name: String,
514        ty_ann: Option<Type>,
515        value: Spanned<ResolvedExpr>,
516    },
517    Expr(Spanned<ResolvedExpr>),
518}
519
520/// Function body — mirror of [`crate::ast::FnBody`].
521#[derive(Debug, Clone, PartialEq)]
522pub enum ResolvedFnBody {
523    Block(Vec<ResolvedStmt>),
524}
525
526impl ResolvedFnBody {
527    pub fn stmts(&self) -> &[ResolvedStmt] {
528        match self {
529            Self::Block(stmts) => stmts,
530        }
531    }
532}
533
534/// Resolved fn definition. Mirrors [`crate::ast::FnDef`] but with
535/// signature types parsed to [`Type`] (and resolved through the
536/// owner module's resolver context, see #148 round 6) instead of
537/// source strings, plus the body lifted to [`ResolvedFnBody`].
538#[derive(Debug, Clone, PartialEq)]
539pub struct ResolvedFnDef {
540    /// Stable opaque identity of this fn. The pre-resolve `FnDef`
541    /// only carried a source name; here we promote identity to a
542    /// first-class field.
543    pub fn_id: FnId,
544    /// Source-level fn name. Kept for diagnostics + backend
545    /// codegen mangling.
546    pub name: String,
547    pub line: usize,
548    /// Parameters: `(binding_name, resolved_param_type)`. The
549    /// resolver canonicalises each annotation through the
550    /// declaring module's own resolver context.
551    pub params: Vec<(String, Type)>,
552    pub return_type: Type,
553    pub effects: Vec<Spanned<String>>,
554    pub desc: Option<String>,
555    pub body: std::sync::Arc<ResolvedFnBody>,
556    /// Slot-resolver output — see [`FnResolution`]. Carried through
557    /// unchanged.
558    pub resolution: Option<FnResolution>,
559}
560
561/// Resolved top-level item — mirror of [`crate::ast::TopLevel`].
562/// `Verify`, `Decision`, `TypeDef` items pass through with their
563/// original AST representation: they aren't on the runtime hot
564/// path, and their internal expressions get resolved lazily by
565/// proof-export passes that already consume `Expr`. Future PRs may
566/// promote them.
567#[derive(Debug, Clone, PartialEq)]
568pub enum ResolvedTopLevel {
569    Module(Module),
570    FnDef(ResolvedFnDef),
571    /// Verify / Decision / TypeDef items: passthrough for now.
572    /// Each carries its original AST node — the resolver lifts only
573    /// what runtime backends consume. See module doc for the
574    /// rationale + the future PR that promotes these.
575    Passthrough(crate::ast::TopLevel),
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use crate::ir::identity::TypeId;
582
583    #[test]
584    fn resolved_callee_user_fn_carries_typed_identity() {
585        let callee = ResolvedCallee::Fn(FnId(7));
586        let call = ResolvedExpr::Call(
587            callee,
588            vec![Spanned::new(ResolvedExpr::Literal(Literal::Int(42)), 3)],
589        );
590        // Smoke check: shape constructs and the FnId survives clone.
591        let clone = call.clone();
592        assert_eq!(clone, call);
593        match clone {
594            ResolvedExpr::Call(ResolvedCallee::Fn(id), args) => {
595                assert_eq!(id, FnId(7));
596                assert_eq!(args.len(), 1);
597            }
598            _ => panic!("expected ResolvedExpr::Call(Fn, _)"),
599        }
600    }
601
602    #[test]
603    fn resolved_ctor_user_carries_ctor_and_type_id() {
604        let ctor = ResolvedCtor::User {
605            ctor_id: CtorId(2),
606            type_id: TypeId(5),
607            name: "Circle".to_string(),
608        };
609        let expr = ResolvedExpr::Ctor(
610            ctor,
611            vec![Spanned::new(ResolvedExpr::Literal(Literal::Float(1.0)), 1)],
612        );
613        let ResolvedExpr::Ctor(
614            ResolvedCtor::User {
615                ctor_id,
616                type_id,
617                name,
618            },
619            ..,
620        ) = expr
621        else {
622            panic!("expected User ctor variant")
623        };
624        assert_eq!(ctor_id, CtorId(2));
625        assert_eq!(type_id, TypeId(5));
626        assert_eq!(name, "Circle");
627    }
628
629    #[test]
630    fn resolved_ctor_builtin_variants_distinguish() {
631        // Round-trip every builtin variant — guard against an
632        // accidental rename / collapse.
633        let variants = [
634            BuiltinCtor::ResultOk,
635            BuiltinCtor::ResultErr,
636            BuiltinCtor::OptionSome,
637            BuiltinCtor::OptionNone,
638        ];
639        for v in variants {
640            let c = ResolvedCtor::Builtin(v);
641            match c {
642                ResolvedCtor::Builtin(got) => assert_eq!(got, v),
643                _ => panic!("builtin ctor lost variant kind"),
644            }
645        }
646    }
647
648    #[test]
649    fn resolved_pattern_ctor_round_trips_through_clone() {
650        let pat = ResolvedPattern::Ctor(
651            ResolvedCtor::User {
652                ctor_id: CtorId(11),
653                type_id: TypeId(3),
654                name: "Square".to_string(),
655            },
656            vec!["side".to_string()],
657        );
658        assert_eq!(pat.clone(), pat);
659    }
660
661    #[test]
662    fn resolved_match_arm_equality_ignores_binding_slots() {
663        // Mirror of `MatchArm`'s structural equality rule — two
664        // arms with the same pattern + body must compare equal
665        // regardless of whether the slot table has been filled in.
666        let body = Box::new(Spanned::new(ResolvedExpr::Literal(Literal::Int(0)), 1));
667        let a = ResolvedMatchArm {
668            pattern: ResolvedPattern::Wildcard,
669            body: body.clone(),
670            binding_slots: std::sync::OnceLock::new(),
671        };
672        let b = ResolvedMatchArm {
673            pattern: ResolvedPattern::Wildcard,
674            body,
675            binding_slots: {
676                let lock = std::sync::OnceLock::new();
677                let _ = lock.set(vec![3, 4]);
678                lock
679            },
680        };
681        assert_eq!(a, b);
682    }
683
684    #[test]
685    fn resolved_fn_def_carries_fn_id_and_resolved_param_types() {
686        let body = ResolvedFnBody::Block(vec![ResolvedStmt::Expr(Spanned::new(
687            ResolvedExpr::Literal(Literal::Int(1)),
688            1,
689        ))]);
690        let def = ResolvedFnDef {
691            fn_id: FnId(0),
692            name: "id".to_string(),
693            line: 1,
694            params: vec![("x".to_string(), Type::Int)],
695            return_type: Type::Int,
696            effects: vec![],
697            desc: None,
698            body: std::sync::Arc::new(body),
699            resolution: None,
700        };
701        assert_eq!(def.fn_id, FnId(0));
702        assert_eq!(def.params[0].1, Type::Int);
703        assert_eq!(def.return_type, Type::Int);
704        assert_eq!(def.body.stmts().len(), 1);
705    }
706
707    #[test]
708    fn resolved_stmt_binding_threads_optional_resolved_type_ann() {
709        // `Stmt::Binding(name, Option<String>, …)` in the pre-Phase-E
710        // AST stored the annotation as a source string. Resolved
711        // form lifts to `Option<Type>`.
712        let stmt_with = ResolvedStmt::Binding {
713            name: "n".to_string(),
714            ty_ann: Some(Type::Int),
715            value: Spanned::new(ResolvedExpr::Literal(Literal::Int(0)), 1),
716        };
717        let stmt_without = ResolvedStmt::Binding {
718            name: "n".to_string(),
719            ty_ann: None,
720            value: Spanned::new(ResolvedExpr::Literal(Literal::Int(0)), 1),
721        };
722        assert_ne!(stmt_with, stmt_without);
723    }
724
725    #[test]
726    fn resolved_callee_unresolved_passes_through_inner_expr() {
727        // The Unresolved escape hatch lets the resolver emit a
728        // placeholder when it can't classify the callee — every
729        // backend uses the same bail-out path. This test just
730        // verifies it constructs and clones.
731        let inner = Spanned::new(ResolvedExpr::Ident("dynamic".to_string()), 1);
732        let call = ResolvedExpr::Call(
733            ResolvedCallee::Unresolved {
734                callee: Box::new(inner.clone()),
735            },
736            vec![],
737        );
738        let clone = call.clone();
739        assert_eq!(clone, call);
740    }
741
742    // Compile-time documentation: the structural mapping
743    // `Expr → ResolvedExpr` is exhaustive — every variant `Expr`
744    // can produce maps to a counterpart here. Asserting this in
745    // code keeps reviewers honest: when a new `Expr` variant lands,
746    // either this list gains a row or the compiler stops building.
747    #[test]
748    fn variant_coverage_matches_expr() {
749        use crate::ast::Expr;
750        fn cover(expr: &Expr) -> &'static str {
751            match expr {
752                Expr::Literal(_) => "Literal → Literal",
753                Expr::Ident(_) => "Ident → Ident",
754                Expr::Resolved { .. } => "Resolved → Resolved",
755                Expr::Attr(_, _) => "Attr → Attr",
756                Expr::FnCall(_, _) => "FnCall → Call",
757                Expr::BinOp(_, _, _) => "BinOp → BinOp",
758                Expr::Neg(_) => "Neg → Neg",
759                Expr::Match { .. } => "Match → Match",
760                Expr::Constructor(_, _) => "Constructor → Ctor",
761                Expr::ErrorProp(_) => "ErrorProp → ErrorProp",
762                Expr::InterpolatedStr(_) => "InterpolatedStr → InterpolatedStr",
763                Expr::List(_) => "List → List",
764                Expr::Tuple(_) => "Tuple → Tuple",
765                Expr::MapLiteral(_) => "MapLiteral → MapLiteral",
766                Expr::RecordCreate { .. } => "RecordCreate → RecordCreate",
767                Expr::RecordUpdate { .. } => "RecordUpdate → RecordUpdate",
768                Expr::TailCall(_) => "TailCall → TailCall",
769                Expr::IndependentProduct(_, _) => "IndependentProduct → IndependentProduct",
770            }
771        }
772        // Trivial probe — the real assertion is the `match`'s
773        // exhaustiveness check at compile time.
774        let probe = Expr::Literal(Literal::Int(0));
775        assert!(cover(&probe).contains("Literal"));
776        // Mirror sentinel — touching every `ResolvedExpr` variant
777        // here keeps the symmetry honest from the other side too.
778        let _: ResolvedExpr = ResolvedExpr::Literal(Literal::Int(0));
779        let _: ResolvedExpr = ResolvedExpr::Ident(String::new());
780        let _: ResolvedExpr = ResolvedExpr::Resolved {
781            slot: 0,
782            name: String::new(),
783            last_use: AnnotBool(false),
784        };
785        let dummy = || Box::new(Spanned::new(ResolvedExpr::Literal(Literal::Int(0)), 1));
786        let _: ResolvedExpr = ResolvedExpr::Attr(dummy(), String::new());
787        let _: ResolvedExpr = ResolvedExpr::Call(ResolvedCallee::Fn(FnId(0)), vec![]);
788        let _: ResolvedExpr = ResolvedExpr::BinOp(BinOp::Add, dummy(), dummy());
789        let _: ResolvedExpr = ResolvedExpr::Neg(dummy());
790        let _: ResolvedExpr = ResolvedExpr::Match {
791            subject: dummy(),
792            arms: vec![],
793        };
794        let _: ResolvedExpr =
795            ResolvedExpr::Ctor(ResolvedCtor::Builtin(BuiltinCtor::OptionNone), vec![]);
796        let _: ResolvedExpr = ResolvedExpr::ErrorProp(dummy());
797        let _: ResolvedExpr = ResolvedExpr::InterpolatedStr(vec![]);
798        let _: ResolvedExpr = ResolvedExpr::List(vec![]);
799        let _: ResolvedExpr = ResolvedExpr::Tuple(vec![]);
800        let _: ResolvedExpr = ResolvedExpr::MapLiteral(vec![]);
801        let _: ResolvedExpr = ResolvedExpr::RecordCreate {
802            type_id: None,
803            type_name: String::new(),
804            fields: vec![],
805        };
806        let _: ResolvedExpr = ResolvedExpr::RecordUpdate {
807            type_id: None,
808            type_name: String::new(),
809            base: dummy(),
810            updates: vec![],
811        };
812        let _: ResolvedExpr = ResolvedExpr::TailCall {
813            target: FnId(0),
814            args: vec![],
815        };
816        let _: ResolvedExpr = ResolvedExpr::IndependentProduct(vec![], false);
817    }
818}