use nova_diagnostics::Diagnostic;
use nova_hir as hir;
use nova_hir::Ty;
use nova_resolver::DefId;
use rustc_hash::FxHashSet;
use crate::lower::lower_function;
use crate::{mangle, mir_ty, ExternDecl, Function, MirTy, Module, Temp};
pub fn lower_module(module: &hir::Module) -> Result<Module, Vec<Diagnostic>> {
let mut diagnostics = Vec::new();
let Some(main) = module.functions.iter().find(|f| f.name == "main") else {
diagnostics.push(Diagnostic::error(
"E0601",
"no `main` function found; `nova run` needs a `fn main()` entry point",
));
return Err(diagnostics);
};
if main.generics != 0 || main.params != 0 {
diagnostics.push(
Diagnostic::error("E0601", "`main` cannot be generic or take parameters")
.with_primary_label(main.span, "invalid `main` signature"),
);
return Err(diagnostics);
}
let entry = main.def_id;
let mut mir = Module::default();
let mut done: FxHashSet<String> = FxHashSet::default();
let mut worklist: Vec<(DefId, Vec<Ty>)> = vec![(entry, Vec::new())];
let mut async_entry: Option<String> = None;
while let Some((def_id, type_args)) = worklist.pop() {
let Some(func) = module.function(def_id) else {
if let Some(ext) = module.extern_fn(def_id) {
let params: Vec<MirTy> = ext.params.iter().map(mir_ty).collect();
let ret = mir_ty(&ext.ret);
if done.insert(ext.symbol.clone()) {
mir.externs.push(ExternDecl {
symbol: ext.symbol.clone(),
params,
ret,
});
} else if let Some(prev) = mir.externs.iter().find(|e| e.symbol == ext.symbol) {
if prev.params != params || prev.ret != ret {
diagnostics.push(
Diagnostic::error(
"E0075",
format!(
"extern symbol `{}` is declared with conflicting signatures",
ext.symbol
),
)
.with_primary_label(ext.span, "conflicting declaration"),
);
}
}
}
continue;
};
let name = if def_id == entry && !func.is_async {
"main".to_string()
} else {
mangle(def_id, &func.name, &type_args)
};
if def_id == entry && func.is_async {
async_entry = Some(name.clone());
}
if !done.insert(name.clone()) {
continue;
}
if type_args.iter().any(Ty::has_params) || type_args.iter().any(Ty::has_vars) {
diagnostics.push(
Diagnostic::error(
"E0011",
format!(
"cannot monomorphize `{}` with non-concrete type arguments",
func.name
),
)
.with_primary_label(func.span, "instantiated here"),
);
continue;
}
let mut bounds_ok = true;
for (i, bounds) in func.bounds.iter().enumerate() {
let Some(arg) = type_args.get(i) else {
continue;
};
for &trait_id in bounds {
let satisfied = impl_satisfies(module, arg, trait_id);
if !satisfied {
bounds_ok = false;
let trait_name = module
.trait_def(trait_id)
.map(|t| t.name.clone())
.unwrap_or_else(|| format!("trait#{}", trait_id.0));
diagnostics.push(
Diagnostic::error(
"E0013",
format!(
"trait bound `{}: {}` is not satisfied when instantiating `{}`",
type_name(arg, module),
trait_name,
func.name
),
)
.with_primary_label(func.span, "required by this generic parameter"),
);
}
}
}
if !bounds_ok {
continue;
}
let mut spec = Specializer {
args: &type_args,
impls: &module.impls,
unresolved: None,
overflow: None,
};
let specialized = spec.function(func);
let mut resolved_ok = true;
if let Some((at, limit)) = spec.overflow {
resolved_ok = false;
let detail = match limit {
hir::NormalizeLimit::Depth => format!(
"the chain of bindings is more than {} deep, or does not terminate",
hir::NORMALIZE_DEPTH_LIMIT,
),
hir::NormalizeLimit::Steps => format!(
"it resolves to more than {} type nodes",
hir::NORMALIZE_STEP_LIMIT,
),
};
diagnostics.push(
Diagnostic::error(
"E0078",
format!(
"could not resolve the associated types in `{}` when instantiating \
`{}`: {}",
type_name(&at, module),
func.name,
detail,
),
)
.with_primary_label(func.span, "instantiated here"),
);
}
if let Some(at) = spec.unresolved {
resolved_ok = false;
diagnostics.push(
Diagnostic::error(
"E0079",
format!(
"`{}` is still an unresolved associated type after instantiating `{}`; \
no impl in scope binds it for this Self type",
type_name(&at, module),
func.name,
),
)
.with_primary_label(func.span, "instantiated here"),
);
}
if !resolved_ok {
continue;
}
let mut request = |def: DefId, args: Vec<Ty>| worklist.push((def, args));
match lower_function(&specialized, &name, module, entry, &mut request) {
Ok(f) => mir.functions.push(f),
Err(d) => diagnostics.extend(d),
}
}
if diagnostics.is_empty() {
if let Some(wrapper) = async_entry {
mir.functions.push(async_main_shim(&wrapper));
}
crate::async_lower::transform(&mut mir);
debug_assert!(
mir.functions.iter().all(|f| !f.is_async),
"the async transform must leave no function flagged: one that reaches \
codegen still flagged is emitted under its own symbol with its BODY's \
return class instead of a future's pointer (see `Function::is_async`)"
);
debug_assert!(
!mir.functions
.iter()
.flat_map(|f| &f.blocks)
.flat_map(|b| &b.stmts)
.any(|s| matches!(s, crate::Stmt::Await { .. })),
"the async transform must consume every await marker: no codegen \
backend can emit one, and a function that is not `is_async` cannot \
have had its awaits split (see `Stmt::Await`)"
);
Ok(mir)
} else {
Err(diagnostics)
}
}
fn async_main_shim(wrapper: &str) -> Function {
let future = Temp(0);
Function {
name: "main".to_string(),
params: 0,
takes_env: false,
capture_count: 0,
temps: vec![MirTy::Ptr],
ret: MirTy::Unit,
is_async: false,
blocks: vec![crate::Block {
stmts: vec![
crate::Stmt::Call {
dst: Some(future),
callee: wrapper.to_string(),
args: Vec::new(),
},
crate::Stmt::CallRuntime {
dst: None,
func: crate::RtFunc::TaskBlockOn,
args: vec![future],
},
],
term: crate::Terminator::Return(None),
}],
}
}
fn impl_satisfies(module: &hir::Module, arg: &Ty, trait_id: DefId) -> bool {
let Some(head) = arg.head() else {
return false;
};
module
.impls
.iter()
.filter(|im| im.trait_id == Some(trait_id) && im.self_head == head)
.any(|imp| match imp.match_args(arg) {
Some(impl_args) => imp.bounds.iter().enumerate().all(|(i, param_bounds)| {
param_bounds.iter().all(|&bound| {
impl_args
.get(i)
.map(|a| impl_satisfies(module, a, bound))
.unwrap_or(false)
})
}),
None => false,
})
}
fn type_name(ty: &Ty, module: &hir::Module) -> String {
match ty {
Ty::Int => "Int".to_string(),
Ty::Float => "Float".to_string(),
Ty::Bool => "Bool".to_string(),
Ty::Char => "Char".to_string(),
Ty::String => "String".to_string(),
Ty::Bytes => "Bytes".to_string(),
Ty::Unit => "()".to_string(),
Ty::Sum { def_id, .. } => module
.sum(*def_id)
.map(|s| s.name.clone())
.unwrap_or_else(|| "?".to_string()),
Ty::Record { def_id, .. } => module
.record(*def_id)
.map(|r| r.name.clone())
.unwrap_or_else(|| "?".to_string()),
Ty::Array(elem) => format!("[{}]", type_name(elem, module)),
Ty::Future(out) => format!("Future<{}>", type_name(out, module)),
Ty::Assoc { on, assoc } => {
let name = module
.traits
.iter()
.flat_map(|t| t.assoc_types.iter())
.find(|(_, d)| d == assoc)
.map(|(n, _)| n.as_str())
.unwrap_or("?");
format!("{}::{}", type_name(on, module), name)
}
_ => "?".to_string(),
}
}
struct Specializer<'a> {
args: &'a [Ty],
impls: &'a [hir::ImplInfo],
unresolved: Option<Ty>,
overflow: Option<(Ty, hir::NormalizeLimit)>,
}
impl Specializer<'_> {
fn ty(&mut self, ty: &Ty) -> Ty {
let substituted = ty.subst(self.args);
if !substituted.has_assoc() {
return substituted;
}
match hir::normalize_ty(&substituted, self.impls) {
Ok(resolved) => {
if resolved.has_assoc() && self.unresolved.is_none() {
self.unresolved = Some(resolved.clone());
}
resolved
}
Err(hir::NormalizeOverflow { at, limit }) => {
if self.overflow.is_none() {
self.overflow = Some((at, limit));
}
substituted
}
}
}
fn function(&mut self, func: &hir::Function) -> hir::Function {
hir::Function {
def_id: func.def_id,
name: func.name.clone(),
generics: 0,
bounds: Vec::new(),
takes_env: func.takes_env,
capture_count: func.capture_count,
params: func.params,
locals: func
.locals
.iter()
.map(|l| hir::Local {
name: l.name.clone(),
ty: self.ty(&l.ty),
is_mut: l.is_mut,
span: l.span,
})
.collect(),
ret_ty: self.ty(&func.ret_ty),
is_async: func.is_async,
body: self.expr(&func.body),
span: func.span,
}
}
}
impl Specializer<'_> {
fn tys(&mut self, tys: &[Ty]) -> Vec<Ty> {
tys.iter().map(|t| self.ty(t)).collect()
}
fn exprs(&mut self, exprs: &[hir::Expr]) -> Vec<hir::Expr> {
exprs.iter().map(|e| self.expr(e)).collect()
}
fn boxed(&mut self, expr: &hir::Expr) -> Box<hir::Expr> {
Box::new(self.expr(expr))
}
fn opt(&mut self, expr: Option<&hir::Expr>) -> Option<Box<hir::Expr>> {
expr.map(|e| self.boxed(e))
}
fn expr(&mut self, expr: &hir::Expr) -> hir::Expr {
use hir::ExprKind as K;
let kind = match &expr.kind {
K::IntLit(v) => K::IntLit(*v),
K::FloatLit(v) => K::FloatLit(*v),
K::BoolLit(v) => K::BoolLit(*v),
K::StrLit(v) => K::StrLit(v.clone()),
K::CharLit(v) => K::CharLit(*v),
K::Unit => K::Unit,
K::Break => K::Break,
K::Continue => K::Continue,
K::Local(l) => K::Local(*l),
K::MakeClosure {
func,
type_args,
captures,
} => K::MakeClosure {
func: *func,
type_args: self.tys(type_args),
captures: self.exprs(captures),
},
K::Call {
func,
type_args,
args: call_args,
} => K::Call {
func: *func,
type_args: self.tys(type_args),
args: self.exprs(call_args),
},
K::MakeVariant {
sum,
variant,
args: v_args,
} => K::MakeVariant {
sum: *sum,
variant: *variant,
args: self.exprs(v_args),
},
K::MakeRecord {
record,
fields: rec_fields,
} => K::MakeRecord {
record: *record,
fields: self.exprs(rec_fields),
},
K::FieldGet { target, index } => K::FieldGet {
target: self.boxed(target),
index: *index,
},
K::FieldSet {
target,
index,
value,
} => K::FieldSet {
target: self.boxed(target),
index: *index,
value: self.boxed(value),
},
K::MakeArray { elems } => K::MakeArray {
elems: self.exprs(elems),
},
K::ArrayRepeat { init, len } => K::ArrayRepeat {
init: self.boxed(init),
len: self.boxed(len),
},
K::Index { target, index } => K::Index {
target: self.boxed(target),
index: self.boxed(index),
},
K::IndexSet {
target,
index,
value,
} => K::IndexSet {
target: self.boxed(target),
index: self.boxed(index),
value: self.boxed(value),
},
K::ArrayLen { target } => K::ArrayLen {
target: self.boxed(target),
},
K::TraitCall {
trait_id,
method,
self_ty,
type_args,
receiver,
args: call_args,
} => K::TraitCall {
trait_id: *trait_id,
method: *method,
self_ty: self.ty(self_ty),
type_args: self.tys(type_args),
receiver: self.opt(receiver.as_deref()),
args: self.exprs(call_args),
},
K::Binary { op, lhs, rhs } => K::Binary {
op: *op,
lhs: self.boxed(lhs),
rhs: self.boxed(rhs),
},
K::LogicalAnd { lhs, rhs } => K::LogicalAnd {
lhs: self.boxed(lhs),
rhs: self.boxed(rhs),
},
K::LogicalOr { lhs, rhs } => K::LogicalOr {
lhs: self.boxed(lhs),
rhs: self.boxed(rhs),
},
K::Unary { op, expr: inner } => K::Unary {
op: *op,
expr: self.boxed(inner),
},
K::Let { local, init } => K::Let {
local: *local,
init: self.boxed(init),
},
K::Assign { local, value } => K::Assign {
local: *local,
value: self.boxed(value),
},
K::Block { stmts, trailing } => K::Block {
stmts: self.exprs(stmts),
trailing: self.opt(trailing.as_deref()),
},
K::If { cond, then, else_ } => K::If {
cond: self.boxed(cond),
then: self.boxed(then),
else_: self.opt(else_.as_deref()),
},
K::While { cond, body } => K::While {
cond: self.boxed(cond),
body: self.boxed(body),
},
K::Match { scrutinee, arms } => K::Match {
scrutinee: self.boxed(scrutinee),
arms: arms
.iter()
.map(|a| hir::Arm {
pattern: a.pattern.clone(),
body: self.expr(&a.body),
span: a.span,
})
.collect(),
},
K::Return(v) => K::Return(self.opt(v.as_deref())),
K::ToStr(inner) => K::ToStr(self.boxed(inner)),
K::StrConcat(parts) => K::StrConcat(self.exprs(parts)),
K::Await(inner) => K::Await(self.boxed(inner)),
};
hir::Expr {
kind,
ty: self.ty(&expr.ty),
span: expr.span,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use nova_diagnostics::{FileId, Span};
use nova_hir::{Callee, Expr, ExprKind as K, ImplInfo, Local, TraitDef, TyHead};
const TRAIT: DefId = DefId(10);
const ITEM: DefId = DefId(11);
const OTHER: DefId = DefId(12);
const REC: DefId = DefId(13);
const MAIN: DefId = DefId(14);
const F: DefId = DefId(15);
fn dummy_span() -> Span {
Span::point(0, FileId::DUMMY)
}
fn w() -> Ty {
Ty::Record {
def_id: REC,
args: Vec::new(),
}
}
fn expr(kind: K, ty: Ty) -> Expr {
Expr {
kind,
ty,
span: dummy_span(),
}
}
fn module_with(binding: DefId) -> hir::Module {
let body = expr(
K::Block {
stmts: vec![expr(
K::Let {
local: nova_hir::LocalId(0),
init: Box::new(expr(K::IntLit(5), Ty::Int)),
},
Ty::Unit,
)],
trailing: None,
},
Ty::Unit,
);
let f = hir::Function {
def_id: F,
name: "f".to_string(),
generics: 1,
bounds: vec![Vec::new()],
takes_env: false,
capture_count: 0,
params: 0,
locals: vec![Local {
name: "y".to_string(),
ty: Ty::Assoc {
on: Box::new(Ty::Param(0)),
assoc: ITEM,
},
is_mut: false,
span: dummy_span(),
}],
ret_ty: Ty::Unit,
is_async: false,
body,
span: dummy_span(),
};
let main = hir::Function {
def_id: MAIN,
name: "main".to_string(),
generics: 0,
bounds: Vec::new(),
takes_env: false,
capture_count: 0,
params: 0,
locals: Vec::new(),
ret_ty: Ty::Unit,
is_async: false,
body: expr(
K::Block {
stmts: vec![expr(
K::Call {
func: Callee::Def(F),
type_args: vec![w()],
args: Vec::new(),
},
Ty::Unit,
)],
trailing: None,
},
Ty::Unit,
),
span: dummy_span(),
};
hir::Module {
sums: Vec::new(),
records: vec![hir::RecordType {
def_id: REC,
name: "W".to_string(),
generics: 0,
fields: Vec::new(),
}],
traits: vec![TraitDef {
def_id: TRAIT,
name: "It".to_string(),
supertraits: Vec::new(),
methods: Vec::new(),
assoc_types: vec![("Item".to_string(), ITEM)],
}],
impls: vec![ImplInfo {
trait_id: Some(TRAIT),
self_head: TyHead::Record(REC),
self_ty: w(),
generics: 0,
bounds: Vec::new(),
methods: Vec::new(),
assoc_bindings: vec![(binding, Ty::Int)],
}],
functions: vec![main, f],
externs: Vec::new(),
}
}
#[test]
fn a_projection_on_a_generic_parameter_resolves_once_the_argument_is_known() {
let module = module_with(ITEM);
let mir = lower_module(&module).expect("no diagnostics");
let f = mir
.functions
.iter()
.find(|f| f.name.starts_with("f."))
.expect("f was instantiated");
assert_eq!(
f.temps.first(),
Some(&MirTy::I64),
"W::Item is Int, so the local is an i64, not a dropped unit"
);
}
#[test]
fn a_projection_that_survives_monomorphization_is_a_diagnostic() {
let module = module_with(OTHER);
let diagnostics = lower_module(&module).expect_err("a surviving projection");
let codes: Vec<&str> = diagnostics.iter().map(|d| d.code.as_str()).collect();
assert_eq!(codes, ["E0079"], "{diagnostics:?}");
assert!(
diagnostics[0].message.contains("`W::Item`"),
"{}",
diagnostics[0].message
);
}
#[test]
fn mangle_ty_distinguishes_futures_by_output_type() {
let a = crate::mangle_ty(&hir::Ty::Future(Box::new(hir::Ty::Int)));
let b = crate::mangle_ty(&hir::Ty::Future(Box::new(hir::Ty::Float)));
let c = crate::mangle_ty(&hir::Ty::Future(Box::new(hir::Ty::Bool)));
assert_ne!(a, b);
assert_ne!(b, c);
assert_ne!(a, c);
assert_ne!(a, crate::mangle_ty(&hir::Ty::Array(Box::new(hir::Ty::Int))));
}
#[test]
fn mangle_ty_gives_bytes_a_distinct_string_from_every_nullary_type_with_its_own_arm() {
let bytes = crate::mangle_ty(&hir::Ty::Bytes);
for (name, other) in [
("Int", hir::Ty::Int),
("Float", hir::Ty::Float),
("Bool", hir::Ty::Bool),
("Char", hir::Ty::Char),
("String", hir::Ty::String),
("Unit", hir::Ty::Unit),
("Never", hir::Ty::Never),
] {
assert_ne!(
bytes,
crate::mangle_ty(&other),
"Bytes's mangled string must not collide with {name}'s"
);
}
}
#[test]
fn mir_ty_maps_future_to_ptr() {
assert_eq!(
crate::mir_ty(&hir::Ty::Future(Box::new(hir::Ty::Int))),
crate::MirTy::Ptr
);
}
}