Skip to main content

nova_mir/
mono.rs

1//! Monomorphization: instantiate generic functions per concrete
2//! type-argument list, reachable from `main`.
3
4use nova_diagnostics::Diagnostic;
5use nova_hir as hir;
6use nova_hir::Ty;
7use nova_resolver::DefId;
8use rustc_hash::FxHashSet;
9
10use crate::lower::lower_function;
11use crate::{mangle, mir_ty, ExternDecl, Function, MirTy, Module, Temp};
12
13/// Lower a typed HIR module to monomorphized MIR.
14///
15/// Instances are discovered from `main` outward, so unreferenced functions
16/// (and never-instantiated generic functions) are not emitted.
17pub fn lower_module(module: &hir::Module) -> Result<Module, Vec<Diagnostic>> {
18    let mut diagnostics = Vec::new();
19    let Some(main) = module.functions.iter().find(|f| f.name == "main") else {
20        diagnostics.push(Diagnostic::error(
21            "E0601",
22            "no `main` function found; `nova run` needs a `fn main()` entry point",
23        ));
24        return Err(diagnostics);
25    };
26    if main.generics != 0 || main.params != 0 {
27        diagnostics.push(
28            Diagnostic::error("E0601", "`main` cannot be generic or take parameters")
29                .with_primary_label(main.span, "invalid `main` signature"),
30        );
31        return Err(diagnostics);
32    }
33
34    let entry = main.def_id;
35    let mut mir = Module::default();
36    let mut done: FxHashSet<String> = FxHashSet::default();
37    let mut worklist: Vec<(DefId, Vec<Ty>)> = vec![(entry, Vec::new())];
38    // The symbol an `async fn main` was lowered under, once it has been
39    // lowered — the future-building wrapper `async_main_shim` calls. `None`
40    // for the ordinary case of a non-async entry point.
41    let mut async_entry: Option<String> = None;
42
43    while let Some((def_id, type_args)) = worklist.pop() {
44        let Some(func) = module.function(def_id) else {
45            // Not a Nova function: if it is an `extern` declaration, record it
46            // (a leaf — no body to lower) so codegen imports its raw C symbol.
47            // Deduped by symbol via the shared `done` set (raw symbols never
48            // collide with `name.<defid>`-mangled function names). Two extern
49            // declarations that share a symbol but disagree on signature would
50            // otherwise collapse to one import and miscompile (or crash codegen)
51            // the mismatched caller, so that is rejected here.
52            if let Some(ext) = module.extern_fn(def_id) {
53                let params: Vec<MirTy> = ext.params.iter().map(mir_ty).collect();
54                let ret = mir_ty(&ext.ret);
55                if done.insert(ext.symbol.clone()) {
56                    mir.externs.push(ExternDecl {
57                        symbol: ext.symbol.clone(),
58                        params,
59                        ret,
60                    });
61                } else if let Some(prev) = mir.externs.iter().find(|e| e.symbol == ext.symbol) {
62                    if prev.params != params || prev.ret != ret {
63                        diagnostics.push(
64                            Diagnostic::error(
65                                "E0075",
66                                format!(
67                                    "extern symbol `{}` is declared with conflicting signatures",
68                                    ext.symbol
69                                ),
70                            )
71                            .with_primary_label(ext.span, "conflicting declaration"),
72                        );
73                    }
74                }
75            }
76            continue;
77        };
78        // The entry point keeps the bare symbol `main` (the backends look it up
79        // by that name); every other function is mangled with its DefId so that
80        // same-named items from different modules never collapse to one symbol.
81        //
82        // An `async fn main` is the one exception, and it is mangled like any
83        // other function: the async transform hands the entry symbol to the
84        // future-building *wrapper*, which allocates a state object, returns a
85        // future and never polls it, while the backends call `main` for its
86        // effects and discard what it returns. So `main` goes to
87        // `async_main_shim` below, which is what actually drives the future.
88        //
89        // Keyed on `is_async`, never on "does `ret_ty` start with `Future`": a
90        // non-async function may itself declare `-> Future<T>` and forward an
91        // async call's result unchanged (see `wrap_fn_value` in `nova-typeck`),
92        // and such a `main` needs no shim — it already returns without
93        // suspending.
94        let name = if def_id == entry && !func.is_async {
95            "main".to_string()
96        } else {
97            mangle(def_id, &func.name, &type_args)
98        };
99        if def_id == entry && func.is_async {
100            async_entry = Some(name.clone());
101        }
102        if !done.insert(name.clone()) {
103            continue;
104        }
105        if type_args.iter().any(Ty::has_params) || type_args.iter().any(Ty::has_vars) {
106            diagnostics.push(
107                Diagnostic::error(
108                    "E0011",
109                    format!(
110                        "cannot monomorphize `{}` with non-concrete type arguments",
111                        func.name
112                    ),
113                )
114                .with_primary_label(func.span, "instantiated here"),
115            );
116            continue;
117        }
118
119        // Check that each concrete type argument satisfies its generic
120        // parameter's trait bounds (spec 12-TYPESYSTEM §5.4: bounds are
121        // verified during monomorphization).
122        let mut bounds_ok = true;
123        for (i, bounds) in func.bounds.iter().enumerate() {
124            let Some(arg) = type_args.get(i) else {
125                continue;
126            };
127            for &trait_id in bounds {
128                let satisfied = impl_satisfies(module, arg, trait_id);
129                if !satisfied {
130                    bounds_ok = false;
131                    let trait_name = module
132                        .trait_def(trait_id)
133                        .map(|t| t.name.clone())
134                        .unwrap_or_else(|| format!("trait#{}", trait_id.0));
135                    diagnostics.push(
136                        Diagnostic::error(
137                            "E0013",
138                            format!(
139                                "trait bound `{}: {}` is not satisfied when instantiating `{}`",
140                                type_name(arg, module),
141                                trait_name,
142                                func.name
143                            ),
144                        )
145                        .with_primary_label(func.span, "required by this generic parameter"),
146                    );
147                }
148            }
149        }
150        // Skip lowering an instance with unsatisfied bounds: its body would
151        // fail to resolve the very trait methods the bound guarantees,
152        // producing a duplicate diagnostic for one root cause.
153        if !bounds_ok {
154            continue;
155        }
156
157        // Specialize the function body for these type arguments, resolving every
158        // associated-type projection the now-concrete arguments make resolvable
159        // (normalization seam 3 — see `Specializer`).
160        let mut spec = Specializer {
161            args: &type_args,
162            impls: &module.impls,
163            unresolved: None,
164            overflow: None,
165        };
166        let specialized = spec.function(func);
167        // A projection that survives to `mir_ty` is mapped to `MirTy::Unit` by
168        // its defensive arm, so the failure is a *unit-typed value where another
169        // type was meant* — not a crash. Measured before this seam existed:
170        // `unwrap_item(W { v: 7 })` printed `0` and `unwrap_item(W { v: true })`
171        // printed `false`, exit code 0, no diagnostic at all; and a projection in
172        // a parameter position dropped the parameter from the Cranelift
173        // signature, so the caller passed two arguments to a one-argument
174        // function and the backend aborted with a verifier error. Both are why
175        // this has to be reported here, where a diagnostic is still possible.
176        let mut resolved_ok = true;
177        if let Some((at, limit)) = spec.overflow {
178            resolved_ok = false;
179            // The same condition and the same remedy as `Checker::normalize`'s
180            // `E0078`, so the same code: it names the error class, not the phase
181            // that noticed. A second code for one condition would be worse.
182            let detail = match limit {
183                hir::NormalizeLimit::Depth => format!(
184                    "the chain of bindings is more than {} deep, or does not terminate",
185                    hir::NORMALIZE_DEPTH_LIMIT,
186                ),
187                hir::NormalizeLimit::Steps => format!(
188                    "it resolves to more than {} type nodes",
189                    hir::NORMALIZE_STEP_LIMIT,
190                ),
191            };
192            diagnostics.push(
193                Diagnostic::error(
194                    "E0078",
195                    format!(
196                        "could not resolve the associated types in `{}` when instantiating \
197                         `{}`: {}",
198                        type_name(&at, module),
199                        func.name,
200                        detail,
201                    ),
202                )
203                .with_primary_label(func.span, "instantiated here"),
204            );
205        }
206        if let Some(at) = spec.unresolved {
207            resolved_ok = false;
208            diagnostics.push(
209                Diagnostic::error(
210                    "E0079",
211                    format!(
212                        "`{}` is still an unresolved associated type after instantiating `{}`; \
213                         no impl in scope binds it for this Self type",
214                        type_name(&at, module),
215                        func.name,
216                    ),
217                )
218                .with_primary_label(func.span, "instantiated here"),
219            );
220        }
221        // Skip lowering, for the same reason an instance with unsatisfied bounds
222        // is skipped: its types are not the types they claim to be, so lowering
223        // would add its own diagnostics — or emit the invalid code this check
224        // exists to prevent — for one root cause.
225        if !resolved_ok {
226            continue;
227        }
228        let mut request = |def: DefId, args: Vec<Ty>| worklist.push((def, args));
229        match lower_function(&specialized, &name, module, entry, &mut request) {
230            Ok(f) => mir.functions.push(f),
231            Err(d) => diagnostics.extend(d),
232        }
233    }
234
235    if diagnostics.is_empty() {
236        // Before the transform, so the shim is in place whichever order the two
237        // are read in: it is not `is_async`, so `transform` walks past it.
238        if let Some(wrapper) = async_entry {
239            mir.functions.push(async_main_shim(&wrapper));
240        }
241        // Run on the finished module rather than per function inside the loop:
242        // it needs no generics resolved and lands once for both codegen
243        // backends.
244        crate::async_lower::transform(&mut mir);
245        debug_assert!(
246            mir.functions.iter().all(|f| !f.is_async),
247            "the async transform must leave no function flagged: one that reaches \
248             codegen still flagged is emitted under its own symbol with its BODY's \
249             return class instead of a future's pointer (see `Function::is_async`)"
250        );
251        debug_assert!(
252            !mir.functions
253                .iter()
254                .flat_map(|f| &f.blocks)
255                .flat_map(|b| &b.stmts)
256                .any(|s| matches!(s, crate::Stmt::Await { .. })),
257            "the async transform must consume every await marker: no codegen \
258             backend can emit one, and a function that is not `is_async` cannot \
259             have had its awaits split (see `Stmt::Await`)"
260        );
261        Ok(mir)
262    } else {
263        Err(diagnostics)
264    }
265}
266
267/// The `main` an `async fn main` gets: call the future-building wrapper
268/// `wrapper`, then hand the future to the executor.
269///
270/// This is what makes `async fn main` mean anything. An `async fn`'s body is
271/// rewritten into a poll function, and the symbol its callers were compiled
272/// against goes to a wrapper that returns a `{ poll_code, state }` future
273/// without polling it — so an entry point named that way would allocate a state
274/// object and exit, running none of the user's code, with exit 0 and no
275/// diagnostic. The wrapper is therefore mangled like any other function (see
276/// `lower_module`) and the entry symbol goes to this shim instead.
277///
278/// Built here rather than in the driver, and as MIR rather than as a
279/// synthesized `hir::Function`, so that every caller of `lower_module` gets it:
280/// `nova check`, `nova run`, `nova build` and this crate's own tests all reach
281/// the same shim, and none of them has to know that an entry point might need
282/// wrapping.
283///
284/// `Stmt::CallRuntime`'s `dst` is `None`, so the executor's `i64` return —
285/// which is the future's output slot — is discarded. That matches what happens
286/// to a non-async `fn main`'s return value, and it is why this shim does not
287/// need to know the output's machine class.
288fn async_main_shim(wrapper: &str) -> Function {
289    let future = Temp(0);
290    Function {
291        name: "main".to_string(),
292        params: 0,
293        takes_env: false,
294        capture_count: 0,
295        temps: vec![MirTy::Ptr],
296        ret: MirTy::Unit,
297        is_async: false,
298        blocks: vec![crate::Block {
299            stmts: vec![
300                crate::Stmt::Call {
301                    dst: Some(future),
302                    callee: wrapper.to_string(),
303                    args: Vec::new(),
304                },
305                crate::Stmt::CallRuntime {
306                    dst: None,
307                    func: crate::RtFunc::TaskBlockOn,
308                    args: vec![future],
309                },
310            ],
311            term: crate::Terminator::Return(None),
312        }],
313    }
314}
315
316/// Whether concrete type `arg` implements `trait_id`. The matching impl's self
317/// type must fit `arg` structurally (so `impl Foo for Pair<Int, Int>` does not
318/// cover `Pair<Int, Bool>`); a generic impl (`impl<T: Bound> Trait for Box<T>`)
319/// additionally requires its own generic bounds to hold for the type arguments
320/// recovered from `arg`.
321///
322/// This recursion is well-founded and needs no depth cap: each recursive call
323/// checks a bound against a type argument that is a strict sub-term of `arg`
324/// (impl self types are always named constructors, so recovered arguments are
325/// proper components), and a finite type has finite nesting.
326fn impl_satisfies(module: &hir::Module, arg: &Ty, trait_id: DefId) -> bool {
327    let Some(head) = arg.head() else {
328        return false;
329    };
330    // Any impl of the trait for this head that both fits `arg` structurally
331    // and whose own generic bounds hold satisfies the requirement. Selecting
332    // by head alone (and committing to the first) would miss a later impl that
333    // actually fits, and coherence guarantees at most one fits anyway.
334    module
335        .impls
336        .iter()
337        .filter(|im| im.trait_id == Some(trait_id) && im.self_head == head)
338        .any(|imp| match imp.match_args(arg) {
339            // For a non-generic or unconstrained impl `bounds` is empty, so
340            // the fit alone suffices; otherwise every parameter's bounds hold.
341            Some(impl_args) => imp.bounds.iter().enumerate().all(|(i, param_bounds)| {
342                param_bounds.iter().all(|&bound| {
343                    impl_args
344                        .get(i)
345                        .map(|a| impl_satisfies(module, a, bound))
346                        .unwrap_or(false)
347                })
348            }),
349            None => false,
350        })
351}
352
353/// A short display name for a type in monomorphization diagnostics.
354///
355/// `nova-typeck`'s `display_ty` is the richer renderer but it needs
356/// `&Definitions`, which mono does not have; this reads names out of the HIR
357/// module instead. A projection is spelled `<on>::Name`, matching `display_ty`,
358/// by finding the owning trait — the associated type's `DefId` belongs to
359/// exactly one, so the scan is unambiguous.
360fn type_name(ty: &Ty, module: &hir::Module) -> String {
361    match ty {
362        Ty::Int => "Int".to_string(),
363        Ty::Float => "Float".to_string(),
364        Ty::Bool => "Bool".to_string(),
365        Ty::Char => "Char".to_string(),
366        Ty::String => "String".to_string(),
367        Ty::Bytes => "Bytes".to_string(),
368        Ty::Unit => "()".to_string(),
369        Ty::Sum { def_id, .. } => module
370            .sum(*def_id)
371            .map(|s| s.name.clone())
372            .unwrap_or_else(|| "?".to_string()),
373        Ty::Record { def_id, .. } => module
374            .record(*def_id)
375            .map(|r| r.name.clone())
376            .unwrap_or_else(|| "?".to_string()),
377        Ty::Array(elem) => format!("[{}]", type_name(elem, module)),
378        Ty::Future(out) => format!("Future<{}>", type_name(out, module)),
379        Ty::Assoc { on, assoc } => {
380            let name = module
381                .traits
382                .iter()
383                .flat_map(|t| t.assoc_types.iter())
384                .find(|(_, d)| d == assoc)
385                .map(|(n, _)| n.as_str())
386                .unwrap_or("?");
387            format!("{}::{}", type_name(on, module), name)
388        }
389        _ => "?".to_string(),
390    }
391}
392
393/// `subst` followed by normalization — **normalization seam 3** (design doc
394/// §4.1), and the reason it belongs here rather than in `nova-typeck`.
395///
396/// Inside a generic body a projection on a generic parameter is genuinely
397/// abstract: `Assoc { on: Param(0) }` has no [`Ty::head`], so no impl can be
398/// selected and seam 1 correctly leaves it alone. Monomorphization is the first
399/// point where `Param(0)` is known, so `subst` turns it into
400/// `Assoc { on: W<Int> }` — which does have a head — and normalization resolves
401/// it to the impl's binding. Both halves have to happen together at every type
402/// in the instance; `subst` alone is what let a projection reach `mir_ty`.
403///
404/// Carries the *first* failure of each kind rather than reporting per type: one
405/// unresolvable projection appears in a parameter type, in the corresponding
406/// local, and again on every expression node that mentions it, so reporting at
407/// each site would give one root cause a dozen diagnostics.
408struct Specializer<'a> {
409    /// The instance's type arguments, indexed by `Param`.
410    args: &'a [Ty],
411    /// The whole impl table. `normalize_ty` takes a plain `&[hir::ImplInfo]`
412    /// slice precisely so this crate can pass `&module.impls` and share the
413    /// identical code with `Checker::normalize`'s `&self.impls` — two copies of
414    /// projection resolution would be free to disagree silently about a *type*.
415    impls: &'a [hir::ImplInfo],
416    /// A projection that normalization could not resolve even with concrete
417    /// type arguments in hand. Reaching `mir_ty` with one is a miscompile
418    /// (§9 risk 1), so it must be a diagnostic.
419    unresolved: Option<Ty>,
420    /// Normalization ran out of budget: `(the type asked about, which limit)`.
421    overflow: Option<(Ty, hir::NormalizeLimit)>,
422}
423
424impl Specializer<'_> {
425    fn ty(&mut self, ty: &Ty) -> Ty {
426        let substituted = ty.subst(self.args);
427        // Not observable — `normalize_ty` on a projection-free type returns an
428        // equal value — but it is a real cost here in a way it is not at the
429        // other two seams: this runs over *every* type of every local and every
430        // expression node of every instance, where conformance runs over a
431        // handful of signatures. Every Nova program compiles all of std, so the
432        // saved tree clones are not hypothetical.
433        if !substituted.has_assoc() {
434            return substituted;
435        }
436        match hir::normalize_ty(&substituted, self.impls) {
437            Ok(resolved) => {
438                if resolved.has_assoc() && self.unresolved.is_none() {
439                    self.unresolved = Some(resolved.clone());
440                }
441                resolved
442            }
443            Err(hir::NormalizeOverflow { at, limit }) => {
444                if self.overflow.is_none() {
445                    self.overflow = Some((at, limit));
446                }
447                // Returned unresolved rather than as `Ty::Error`: the caller
448                // skips lowering this instance entirely because a diagnostic was
449                // recorded, so nothing reads it, and `Ty::Error` would be a
450                // second lie on top of the one being reported.
451                substituted
452            }
453        }
454    }
455
456    /// Clone a function with every type substituted and normalized: locals,
457    /// signature, and body, including the type arguments recorded at nested call
458    /// sites — those pick the callee's instance, so a projection left in one
459    /// would monomorphize the wrong function.
460    fn function(&mut self, func: &hir::Function) -> hir::Function {
461        hir::Function {
462            def_id: func.def_id,
463            name: func.name.clone(),
464            generics: 0,
465            bounds: Vec::new(),
466            takes_env: func.takes_env,
467            capture_count: func.capture_count,
468            params: func.params,
469            locals: func
470                .locals
471                .iter()
472                .map(|l| hir::Local {
473                    name: l.name.clone(),
474                    ty: self.ty(&l.ty),
475                    is_mut: l.is_mut,
476                    span: l.span,
477                })
478                .collect(),
479            ret_ty: self.ty(&func.ret_ty),
480            is_async: func.is_async,
481            body: self.expr(&func.body),
482            span: func.span,
483        }
484    }
485}
486
487/// Helper methods so a struct literal below can call several of them in one
488/// expression: each returns an owned value, so its `&mut self` borrow ends with
489/// the call.
490impl Specializer<'_> {
491    fn tys(&mut self, tys: &[Ty]) -> Vec<Ty> {
492        tys.iter().map(|t| self.ty(t)).collect()
493    }
494
495    fn exprs(&mut self, exprs: &[hir::Expr]) -> Vec<hir::Expr> {
496        exprs.iter().map(|e| self.expr(e)).collect()
497    }
498
499    fn boxed(&mut self, expr: &hir::Expr) -> Box<hir::Expr> {
500        Box::new(self.expr(expr))
501    }
502
503    fn opt(&mut self, expr: Option<&hir::Expr>) -> Option<Box<hir::Expr>> {
504        expr.map(|e| self.boxed(e))
505    }
506
507    fn expr(&mut self, expr: &hir::Expr) -> hir::Expr {
508        use hir::ExprKind as K;
509        let kind = match &expr.kind {
510            K::IntLit(v) => K::IntLit(*v),
511            K::FloatLit(v) => K::FloatLit(*v),
512            K::BoolLit(v) => K::BoolLit(*v),
513            K::StrLit(v) => K::StrLit(v.clone()),
514            K::CharLit(v) => K::CharLit(*v),
515            K::Unit => K::Unit,
516            K::Break => K::Break,
517            K::Continue => K::Continue,
518            K::Local(l) => K::Local(*l),
519            K::MakeClosure {
520                func,
521                type_args,
522                captures,
523            } => K::MakeClosure {
524                func: *func,
525                type_args: self.tys(type_args),
526                captures: self.exprs(captures),
527            },
528            K::Call {
529                func,
530                type_args,
531                args: call_args,
532            } => K::Call {
533                func: *func,
534                type_args: self.tys(type_args),
535                args: self.exprs(call_args),
536            },
537            K::MakeVariant {
538                sum,
539                variant,
540                args: v_args,
541            } => K::MakeVariant {
542                sum: *sum,
543                variant: *variant,
544                args: self.exprs(v_args),
545            },
546            K::MakeRecord {
547                record,
548                fields: rec_fields,
549            } => K::MakeRecord {
550                record: *record,
551                fields: self.exprs(rec_fields),
552            },
553            K::FieldGet { target, index } => K::FieldGet {
554                target: self.boxed(target),
555                index: *index,
556            },
557            K::FieldSet {
558                target,
559                index,
560                value,
561            } => K::FieldSet {
562                target: self.boxed(target),
563                index: *index,
564                value: self.boxed(value),
565            },
566            K::MakeArray { elems } => K::MakeArray {
567                elems: self.exprs(elems),
568            },
569            K::ArrayRepeat { init, len } => K::ArrayRepeat {
570                init: self.boxed(init),
571                len: self.boxed(len),
572            },
573            K::Index { target, index } => K::Index {
574                target: self.boxed(target),
575                index: self.boxed(index),
576            },
577            K::IndexSet {
578                target,
579                index,
580                value,
581            } => K::IndexSet {
582                target: self.boxed(target),
583                index: self.boxed(index),
584                value: self.boxed(value),
585            },
586            K::ArrayLen { target } => K::ArrayLen {
587                target: self.boxed(target),
588            },
589            K::TraitCall {
590                trait_id,
591                method,
592                self_ty,
593                type_args,
594                receiver,
595                args: call_args,
596            } => K::TraitCall {
597                trait_id: *trait_id,
598                method: *method,
599                self_ty: self.ty(self_ty),
600                type_args: self.tys(type_args),
601                // `None` for a trait associated function; substitution must not
602                // invent a receiver for one.
603                receiver: self.opt(receiver.as_deref()),
604                args: self.exprs(call_args),
605            },
606            K::Binary { op, lhs, rhs } => K::Binary {
607                op: *op,
608                lhs: self.boxed(lhs),
609                rhs: self.boxed(rhs),
610            },
611            K::LogicalAnd { lhs, rhs } => K::LogicalAnd {
612                lhs: self.boxed(lhs),
613                rhs: self.boxed(rhs),
614            },
615            K::LogicalOr { lhs, rhs } => K::LogicalOr {
616                lhs: self.boxed(lhs),
617                rhs: self.boxed(rhs),
618            },
619            K::Unary { op, expr: inner } => K::Unary {
620                op: *op,
621                expr: self.boxed(inner),
622            },
623            K::Let { local, init } => K::Let {
624                local: *local,
625                init: self.boxed(init),
626            },
627            K::Assign { local, value } => K::Assign {
628                local: *local,
629                value: self.boxed(value),
630            },
631            K::Block { stmts, trailing } => K::Block {
632                stmts: self.exprs(stmts),
633                trailing: self.opt(trailing.as_deref()),
634            },
635            K::If { cond, then, else_ } => K::If {
636                cond: self.boxed(cond),
637                then: self.boxed(then),
638                else_: self.opt(else_.as_deref()),
639            },
640            K::While { cond, body } => K::While {
641                cond: self.boxed(cond),
642                body: self.boxed(body),
643            },
644            K::Match { scrutinee, arms } => K::Match {
645                scrutinee: self.boxed(scrutinee),
646                arms: arms
647                    .iter()
648                    .map(|a| hir::Arm {
649                        pattern: a.pattern.clone(),
650                        body: self.expr(&a.body),
651                        span: a.span,
652                    })
653                    .collect(),
654            },
655            K::Return(v) => K::Return(self.opt(v.as_deref())),
656            K::ToStr(inner) => K::ToStr(self.boxed(inner)),
657            K::StrConcat(parts) => K::StrConcat(self.exprs(parts)),
658            K::Await(inner) => K::Await(self.boxed(inner)),
659        };
660        hir::Expr {
661            kind,
662            ty: self.ty(&expr.ty),
663            span: expr.span,
664        }
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use nova_diagnostics::{FileId, Span};
672    use nova_hir::{Callee, Expr, ExprKind as K, ImplInfo, Local, TraitDef, TyHead};
673
674    const TRAIT: DefId = DefId(10);
675    const ITEM: DefId = DefId(11);
676    const OTHER: DefId = DefId(12);
677    const REC: DefId = DefId(13);
678    const MAIN: DefId = DefId(14);
679    const F: DefId = DefId(15);
680
681    fn dummy_span() -> Span {
682        Span::point(0, FileId::DUMMY)
683    }
684
685    fn w() -> Ty {
686        Ty::Record {
687            def_id: REC,
688            args: Vec::new(),
689        }
690    }
691
692    fn expr(kind: K, ty: Ty) -> Expr {
693        Expr {
694            kind,
695            ty,
696            span: dummy_span(),
697        }
698    }
699
700    /// A module where `main` calls `f::<W>`, and `f` holds one local whose
701    /// declared type is the projection `Param(0)::Item`.
702    ///
703    /// `binding` is what `impl ? for W` binds, keyed by associated type — so
704    /// passing `ITEM` gives an impl that answers the projection and passing
705    /// `OTHER` gives one that shares the head but binds something else. That is
706    /// the difference between a projection this seam resolves and one that
707    /// survives it, with everything else held equal.
708    fn module_with(binding: DefId) -> hir::Module {
709        // `let y = 5` where `y: Param(0)::Item`. Nothing else, so the only thing
710        // under test is what that type becomes.
711        let body = expr(
712            K::Block {
713                stmts: vec![expr(
714                    K::Let {
715                        local: nova_hir::LocalId(0),
716                        init: Box::new(expr(K::IntLit(5), Ty::Int)),
717                    },
718                    Ty::Unit,
719                )],
720                trailing: None,
721            },
722            Ty::Unit,
723        );
724        let f = hir::Function {
725            def_id: F,
726            name: "f".to_string(),
727            generics: 1,
728            // No bounds: `E0013` is a different check and would mask this one.
729            bounds: vec![Vec::new()],
730            takes_env: false,
731            capture_count: 0,
732            params: 0,
733            locals: vec![Local {
734                name: "y".to_string(),
735                ty: Ty::Assoc {
736                    on: Box::new(Ty::Param(0)),
737                    assoc: ITEM,
738                },
739                is_mut: false,
740                span: dummy_span(),
741            }],
742            ret_ty: Ty::Unit,
743            is_async: false,
744            body,
745            span: dummy_span(),
746        };
747        let main = hir::Function {
748            def_id: MAIN,
749            name: "main".to_string(),
750            generics: 0,
751            bounds: Vec::new(),
752            takes_env: false,
753            capture_count: 0,
754            params: 0,
755            locals: Vec::new(),
756            ret_ty: Ty::Unit,
757            is_async: false,
758            body: expr(
759                K::Block {
760                    stmts: vec![expr(
761                        K::Call {
762                            func: Callee::Def(F),
763                            type_args: vec![w()],
764                            args: Vec::new(),
765                        },
766                        Ty::Unit,
767                    )],
768                    trailing: None,
769                },
770                Ty::Unit,
771            ),
772            span: dummy_span(),
773        };
774        hir::Module {
775            sums: Vec::new(),
776            records: vec![hir::RecordType {
777                def_id: REC,
778                name: "W".to_string(),
779                generics: 0,
780                fields: Vec::new(),
781            }],
782            traits: vec![TraitDef {
783                def_id: TRAIT,
784                name: "It".to_string(),
785                supertraits: Vec::new(),
786                methods: Vec::new(),
787                assoc_types: vec![("Item".to_string(), ITEM)],
788            }],
789            impls: vec![ImplInfo {
790                trait_id: Some(TRAIT),
791                self_head: TyHead::Record(REC),
792                self_ty: w(),
793                generics: 0,
794                bounds: Vec::new(),
795                methods: Vec::new(),
796                assoc_bindings: vec![(binding, Ty::Int)],
797            }],
798            functions: vec![main, f],
799            externs: Vec::new(),
800        }
801    }
802
803    #[test]
804    fn a_projection_on_a_generic_parameter_resolves_once_the_argument_is_known() {
805        // The positive half of seam 3, at the level the diagnostic lives: `f`'s
806        // local is declared `Param(0)::Item`, the instance is `f::<W>`, and the
807        // impl binds `Item = Int`. So the lowered temp must be `I64`.
808        //
809        // `MirTy::Unit` is the discriminating value, not an arbitrary one: it is
810        // exactly what `mir_ty`'s defensive arm returns for an unresolved
811        // `Assoc`, so this assertion fails precisely when the projection reaches
812        // codegen unresolved.
813        let module = module_with(ITEM);
814        let mir = lower_module(&module).expect("no diagnostics");
815        let f = mir
816            .functions
817            .iter()
818            .find(|f| f.name.starts_with("f."))
819            .expect("f was instantiated");
820        assert_eq!(
821            f.temps.first(),
822            Some(&MirTy::I64),
823            "W::Item is Int, so the local is an i64, not a dropped unit"
824        );
825    }
826
827    #[test]
828    fn a_projection_that_survives_monomorphization_is_a_diagnostic() {
829        // The same module with the impl binding a *different* associated type, so
830        // no binding answers `W::Item` and normalization returns the projection
831        // unchanged. Without the check this reaches `mir_ty`, which maps it to
832        // `MirTy::Unit` — a unit-typed value where an `Int` was meant, with no
833        // diagnostic at all. That is spec §9's risk 1, and it is the one failure
834        // mode this seam exists to make impossible.
835        //
836        // Constructed here rather than in a `.nova` file because **it is not
837        // reachable from source**. Seven probes were tried and each is closed by
838        // an earlier diagnostic: an unresolvable projection at a concrete call
839        // site is `E0010` from seam 1, a supertrait whose impl does not fit
840        // structurally is `E0072`, and an impl generic that the self type does
841        // not mention is `E0073` — that last one being the only route that would
842        // otherwise reach here, via `match_args` resolving the unused parameter
843        // to `Ty::Error`. Instrumenting this branch and running the whole suite
844        // reached it zero times. So it is a backstop, and a backstop with no test
845        // is exactly what Task 5's review found twice.
846        let module = module_with(OTHER);
847        let diagnostics = lower_module(&module).expect_err("a surviving projection");
848        let codes: Vec<&str> = diagnostics.iter().map(|d| d.code.as_str()).collect();
849        assert_eq!(codes, ["E0079"], "{diagnostics:?}");
850        // The message has to name the projection. A diagnostic that says only
851        // "could not resolve an associated type" leaves the user with nothing to
852        // look at, and `type_name` returning `?` for `Assoc` (as it did before
853        // this task) would still satisfy an assertion on the code alone.
854        assert!(
855            diagnostics[0].message.contains("`W::Item`"),
856            "{}",
857            diagnostics[0].message
858        );
859    }
860
861    #[test]
862    fn mangle_ty_distinguishes_futures_by_output_type() {
863        // A Future's mangled name must depend on its output type. `Ty::Assoc`
864        // mangling to a constant "X" already shipped as a miscompile on this
865        // project: two instantiations collided on one symbol and both dispatched
866        // to the first's code. A constant here reproduces that exactly.
867        let a = crate::mangle_ty(&hir::Ty::Future(Box::new(hir::Ty::Int)));
868        let b = crate::mangle_ty(&hir::Ty::Future(Box::new(hir::Ty::Float)));
869        let c = crate::mangle_ty(&hir::Ty::Future(Box::new(hir::Ty::Bool)));
870        assert_ne!(a, b);
871        assert_ne!(b, c);
872        assert_ne!(a, c);
873        // And distinct from the array mangling of the same element type, so the
874        // two single-argument constructors cannot collide either.
875        assert_ne!(a, crate::mangle_ty(&hir::Ty::Array(Box::new(hir::Ty::Int))));
876    }
877
878    #[test]
879    fn mangle_ty_gives_bytes_a_distinct_string_from_every_nullary_type_with_its_own_arm() {
880        // `mangle_ty` mapping two types to the same string has already shipped
881        // as a miscompile on this project (see
882        // `mangle_ty_distinguishes_futures_by_output_type` above): two
883        // monomorphized instances collide on one symbol and both dispatch to
884        // the first one's code. `Bytes` reaching this function is not
885        // hypothetical or Task-2-only: a value-less generic instantiation --
886        // a zero-field generic record's constructor called at `Box<Bytes>`,
887        // the same pattern `tests/runtime/assoc_types.nova` already uses for
888        // `Vec::new()` -- calls `mangle_ty(&Ty::Bytes)` for the symbol name
889        // with no `Bytes` value ever constructed. Checked against every other
890        // nullary type with its own `mangle_ty` arm, not just `String` (the
891        // structural twin it is most likely to be confused with): a
892        // collision with `Int` or `Char` would be exactly as wrong, and this
893        // loop covers all seven of them. `Ty::Error` (also nullary) is
894        // deliberately not among them -- it shares the pre-existing `"X"`
895        // catch-all with `Param`/`Var`/`Assoc`, a latent, already-documented
896        // collision (see `mangle_ty`'s own comment on that arm) this test
897        // does not touch.
898        let bytes = crate::mangle_ty(&hir::Ty::Bytes);
899        for (name, other) in [
900            ("Int", hir::Ty::Int),
901            ("Float", hir::Ty::Float),
902            ("Bool", hir::Ty::Bool),
903            ("Char", hir::Ty::Char),
904            ("String", hir::Ty::String),
905            ("Unit", hir::Ty::Unit),
906            ("Never", hir::Ty::Never),
907        ] {
908            assert_ne!(
909                bytes,
910                crate::mangle_ty(&other),
911                "Bytes's mangled string must not collide with {name}'s"
912            );
913        }
914    }
915
916    #[test]
917    fn mir_ty_maps_future_to_ptr() {
918        // A future value is the fat pointer { poll_code, state_ptr }.
919        // MirTy::Unit would be catastrophic and silent: unit parameters are
920        // DROPPED from the Cranelift signature, which is how the 2.2c projection
921        // bug produced wrong values with exit 0 and no diagnostic.
922        assert_eq!(
923            crate::mir_ty(&hir::Ty::Future(Box::new(hir::Ty::Int))),
924            crate::MirTy::Ptr
925        );
926    }
927}