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}