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}