use std::sync::Arc;
use crate::ast::{Expr, FnBody, FnDef, MatchArm, Pattern, Spanned, Stmt, TopLevel};
use crate::ir::calls::{expr_to_dotted_name, is_builtin_namespace};
use crate::ir::hir::{
BuiltinCtor, BuiltinIntrinsic, ResolvedCallee, ResolvedCtor, ResolvedExpr, ResolvedFnBody,
ResolvedFnDef, ResolvedMatchArm, ResolvedPattern, ResolvedStmt, ResolvedStrPart,
ResolvedTopLevel,
};
use crate::ir::identity::{FnId, FnKey, TypeId, TypeKey};
use crate::ir::symbol_table::SymbolTable;
pub struct ResolveCtx<'a> {
pub symbols: &'a SymbolTable,
pub current_module: Option<String>,
}
impl<'a> ResolveCtx<'a> {
pub fn new(symbols: &'a SymbolTable) -> Self {
Self {
symbols,
current_module: None,
}
}
pub fn resolve_fn_id(&self, name: &str) -> Option<FnId> {
if let Some((prefix, n)) = name.rsplit_once('.') {
if let Some(id) = self.symbols.fn_id_of(&FnKey::in_module(prefix, n)) {
return Some(id);
}
if self.current_module.as_deref() == Some(prefix)
&& let Some(id) = self.symbols.fn_id_of(&FnKey::entry(n))
{
return Some(id);
}
}
if let Some(prefix) = self.current_module.as_deref()
&& let Some(id) = self.symbols.fn_id_of(&FnKey::in_module(prefix, name))
{
return Some(id);
}
self.symbols.fn_id_of(&FnKey::entry(name))
}
pub fn resolve_type_id(&self, name: &str) -> Option<TypeId> {
if let Some((prefix, n)) = name.rsplit_once('.') {
if let Some(id) = self.symbols.type_id_of(&TypeKey::in_module(prefix, n)) {
return Some(id);
}
if self.current_module.as_deref() == Some(prefix)
&& let Some(id) = self.symbols.type_id_of(&TypeKey::entry(n))
{
return Some(id);
}
}
if let Some(prefix) = self.current_module.as_deref()
&& let Some(id) = self.symbols.type_id_of(&TypeKey::in_module(prefix, name))
{
return Some(id);
}
if let Some(id) = self.symbols.type_id_of(&TypeKey::entry(name)) {
return Some(id);
}
self.symbols.type_id_by_bare_name(name)
}
pub fn resolve_user_ctor(
&self,
dotted: &str,
) -> Option<(crate::ir::identity::CtorId, TypeId, String)> {
let (type_name, variant) = dotted.rsplit_once('.')?;
let type_id = self.resolve_type_id(type_name)?;
let ctor_id = self.symbols.ctor_id_of(type_id, variant)?;
Some((ctor_id, type_id, variant.to_string()))
}
}
pub fn resolve_program(symbols: &SymbolTable, items: &[TopLevel]) -> Vec<ResolvedTopLevel> {
let module_name = items.iter().find_map(|i| match i {
TopLevel::Module(m) => Some(m.name.clone()),
_ => None,
});
let mut ctx = ResolveCtx::new(symbols);
ctx.current_module = module_name;
items.iter().map(|i| resolve_top_level(&ctx, i)).collect()
}
pub fn resolve_fn_def_external(ctx: &ResolveCtx<'_>, fd: &FnDef) -> Option<ResolvedFnDef> {
resolve_fn_def(ctx, fd)
}
pub fn resolve_top_level(ctx: &ResolveCtx<'_>, item: &TopLevel) -> ResolvedTopLevel {
match item {
TopLevel::Module(m) => ResolvedTopLevel::Module(m.clone()),
TopLevel::FnDef(fd) => match resolve_fn_def(ctx, fd) {
Some(rfd) => ResolvedTopLevel::FnDef(rfd),
None => ResolvedTopLevel::Passthrough(item.clone()),
},
TopLevel::Verify(_) | TopLevel::Decision(_) | TopLevel::Stmt(_) | TopLevel::TypeDef(_) => {
ResolvedTopLevel::Passthrough(item.clone())
}
}
}
fn resolve_fn_def(ctx: &ResolveCtx<'_>, fd: &FnDef) -> Option<ResolvedFnDef> {
let fn_id = ctx.resolve_fn_id(&fd.name)?;
let params: Vec<(String, crate::ast::Type)> = fd
.params
.iter()
.map(|(name, ann)| {
let ty = match crate::types::parse_type_str_strict(ann) {
Ok(t) => canonicalise_type(ctx, t),
Err(_) => crate::ast::Type::Invalid,
};
(name.clone(), ty)
})
.collect();
let return_type = match crate::types::parse_type_str_strict(&fd.return_type) {
Ok(t) => canonicalise_type(ctx, t),
Err(_) => crate::ast::Type::Invalid,
};
let body = resolve_fn_body(ctx, &fd.body);
Some(ResolvedFnDef {
fn_id,
name: fd.name.clone(),
line: fd.line,
params,
return_type,
effects: fd.effects.clone(),
desc: fd.desc.clone(),
body: Arc::new(body),
resolution: fd.resolution.clone(),
})
}
fn resolve_fn_body(ctx: &ResolveCtx<'_>, body: &FnBody) -> ResolvedFnBody {
match body {
FnBody::Block(stmts) => {
ResolvedFnBody::Block(stmts.iter().map(|s| resolve_stmt(ctx, s)).collect())
}
}
}
pub fn resolve_stmt_external(ctx: &ResolveCtx<'_>, stmt: &Stmt) -> ResolvedStmt {
resolve_stmt(ctx, stmt)
}
fn resolve_stmt(ctx: &ResolveCtx<'_>, stmt: &Stmt) -> ResolvedStmt {
match stmt {
Stmt::Binding(name, ann, expr) => {
let ty_ann = ann
.as_deref()
.map(|src| match crate::types::parse_type_str_strict(src) {
Ok(t) => canonicalise_type(ctx, t),
Err(_) => crate::ast::Type::Invalid,
});
ResolvedStmt::Binding {
name: name.clone(),
ty_ann,
value: resolve_spanned(ctx, expr),
}
}
Stmt::Expr(expr) => ResolvedStmt::Expr(resolve_spanned(ctx, expr)),
}
}
fn resolve_spanned(ctx: &ResolveCtx<'_>, expr: &Spanned<Expr>) -> Spanned<ResolvedExpr> {
let resolved = resolve_expr(ctx, expr);
let out = Spanned::new(resolved, expr.line);
if let Some(t) = expr.ty.get() {
let _ = out.ty.set(t.clone());
}
out
}
fn resolve_expr(ctx: &ResolveCtx<'_>, expr: &Spanned<Expr>) -> ResolvedExpr {
match &expr.node {
Expr::Literal(l) => ResolvedExpr::Literal(l.clone()),
Expr::Ident(name) => ResolvedExpr::Ident(name.clone()),
Expr::Resolved {
slot,
name,
last_use,
} => ResolvedExpr::Resolved {
slot: *slot,
name: name.clone(),
last_use: *last_use,
},
Expr::Attr(obj, field) => {
if let Expr::Ident(ns) = &obj.node {
let qualified = format!("{ns}.{field}");
let ctor = classify_ctor(ctx, &qualified);
let nullary = matches!(
ctor,
ResolvedCtor::Builtin(BuiltinCtor::OptionNone) | ResolvedCtor::User { .. }
);
if nullary {
return ResolvedExpr::Ctor(ctor, vec![]);
}
}
ResolvedExpr::Attr(Box::new(resolve_spanned(ctx, obj)), field.clone())
}
Expr::FnCall(callee, args) => {
let resolved_args: Vec<Spanned<ResolvedExpr>> =
args.iter().map(|a| resolve_spanned(ctx, a)).collect();
if let Some(dotted) = expr_to_dotted_name(&callee.node) {
if let Some(builtin) = parse_builtin_ctor(&dotted) {
return ResolvedExpr::Ctor(ResolvedCtor::Builtin(builtin), resolved_args);
}
if let Some((ctor_id, type_id, variant_name)) = ctx.resolve_user_ctor(&dotted) {
return ResolvedExpr::Ctor(
ResolvedCtor::User {
ctor_id,
type_id,
name: variant_name,
},
resolved_args,
);
}
}
let resolved_callee = classify_callee(ctx, callee);
ResolvedExpr::Call(resolved_callee, resolved_args)
}
Expr::BinOp(op, l, r) => ResolvedExpr::BinOp(
*op,
Box::new(resolve_spanned(ctx, l)),
Box::new(resolve_spanned(ctx, r)),
),
Expr::Neg(inner) => ResolvedExpr::Neg(Box::new(resolve_spanned(ctx, inner))),
Expr::Match { subject, arms } => ResolvedExpr::Match {
subject: Box::new(resolve_spanned(ctx, subject)),
arms: arms.iter().map(|a| resolve_match_arm(ctx, a)).collect(),
},
Expr::Constructor(name, arg) => {
let ctor = classify_ctor(ctx, name);
let args = match arg {
Some(a) => vec![resolve_spanned(ctx, a)],
None => vec![],
};
ResolvedExpr::Ctor(ctor, args)
}
Expr::ErrorProp(inner) => ResolvedExpr::ErrorProp(Box::new(resolve_spanned(ctx, inner))),
Expr::InterpolatedStr(parts) => ResolvedExpr::InterpolatedStr(
parts
.iter()
.map(|p| match p {
crate::ast::StrPart::Literal(s) => ResolvedStrPart::Literal(s.clone()),
crate::ast::StrPart::Parsed(inner) => {
ResolvedStrPart::Parsed(Box::new(resolve_spanned(ctx, inner)))
}
})
.collect(),
),
Expr::List(items) => {
ResolvedExpr::List(items.iter().map(|i| resolve_spanned(ctx, i)).collect())
}
Expr::Tuple(items) => {
ResolvedExpr::Tuple(items.iter().map(|i| resolve_spanned(ctx, i)).collect())
}
Expr::MapLiteral(pairs) => ResolvedExpr::MapLiteral(
pairs
.iter()
.map(|(k, v)| (resolve_spanned(ctx, k), resolve_spanned(ctx, v)))
.collect(),
),
Expr::RecordCreate { type_name, fields } => ResolvedExpr::RecordCreate {
type_id: ctx.resolve_type_id(type_name),
type_name: type_name.clone(),
fields: fields
.iter()
.map(|(n, e)| (n.clone(), resolve_spanned(ctx, e)))
.collect(),
},
Expr::RecordUpdate {
type_name,
base,
updates,
} => ResolvedExpr::RecordUpdate {
type_id: ctx.resolve_type_id(type_name),
type_name: type_name.clone(),
base: Box::new(resolve_spanned(ctx, base)),
updates: updates
.iter()
.map(|(n, e)| (n.clone(), resolve_spanned(ctx, e)))
.collect(),
},
Expr::TailCall(data) => {
let target = ctx.resolve_fn_id(&data.target);
let args = data.args.iter().map(|a| resolve_spanned(ctx, a)).collect();
match target {
Some(fn_id) => ResolvedExpr::TailCall {
target: fn_id,
args,
},
None => ResolvedExpr::Call(
ResolvedCallee::Unresolved {
callee: Box::new(Spanned::new(
ResolvedExpr::Ident(data.target.clone()),
expr.line,
)),
},
args,
),
}
}
Expr::IndependentProduct(items, unwrap) => ResolvedExpr::IndependentProduct(
items.iter().map(|i| resolve_spanned(ctx, i)).collect(),
*unwrap,
),
}
}
fn resolve_match_arm(ctx: &ResolveCtx<'_>, arm: &MatchArm) -> ResolvedMatchArm {
let binding_slots = std::sync::OnceLock::new();
if let Some(slots) = arm.binding_slots.get() {
let _ = binding_slots.set(slots.clone());
}
ResolvedMatchArm {
pattern: resolve_pattern(ctx, &arm.pattern),
body: Box::new(resolve_spanned(ctx, &arm.body)),
binding_slots,
}
}
fn resolve_pattern(ctx: &ResolveCtx<'_>, pat: &Pattern) -> ResolvedPattern {
match pat {
Pattern::Wildcard => ResolvedPattern::Wildcard,
Pattern::Literal(l) => ResolvedPattern::Literal(l.clone()),
Pattern::Ident(name) => ResolvedPattern::Ident(name.clone()),
Pattern::EmptyList => ResolvedPattern::EmptyList,
Pattern::Cons(head, tail) => ResolvedPattern::Cons(head.clone(), tail.clone()),
Pattern::Tuple(items) => {
ResolvedPattern::Tuple(items.iter().map(|p| resolve_pattern(ctx, p)).collect())
}
Pattern::Constructor(name, bindings) => {
ResolvedPattern::Ctor(classify_ctor(ctx, name), bindings.clone())
}
}
}
fn classify_callee(ctx: &ResolveCtx<'_>, callee: &Spanned<Expr>) -> ResolvedCallee {
match &callee.node {
Expr::Resolved {
slot,
name,
last_use,
} => ResolvedCallee::LocalSlot {
slot: *slot,
name: name.clone(),
last_use: *last_use,
},
Expr::Ident(name) => {
if let Some(intrinsic) = BuiltinIntrinsic::from_name(name) {
return ResolvedCallee::Intrinsic(intrinsic);
}
if let Some((head, _)) = name.split_once('.')
&& is_builtin_namespace(head)
{
return ResolvedCallee::Builtin(name.clone());
}
match ctx.resolve_fn_id(name) {
Some(id) => ResolvedCallee::Fn(id),
None => ResolvedCallee::Unresolved {
callee: Box::new(resolve_spanned(ctx, callee)),
},
}
}
Expr::Attr(_, _) => {
let Some(dotted) = expr_to_dotted_name(&callee.node) else {
return ResolvedCallee::Unresolved {
callee: Box::new(resolve_spanned(ctx, callee)),
};
};
if let Some(id) = ctx.resolve_fn_id(&dotted) {
return ResolvedCallee::Fn(id);
}
if let Some((head, _rest)) = dotted.split_once('.')
&& is_builtin_namespace(head)
{
return ResolvedCallee::Builtin(dotted);
}
ResolvedCallee::Unresolved {
callee: Box::new(resolve_spanned(ctx, callee)),
}
}
_ => ResolvedCallee::Unresolved {
callee: Box::new(resolve_spanned(ctx, callee)),
},
}
}
fn classify_ctor(ctx: &ResolveCtx<'_>, name: &str) -> ResolvedCtor {
if let Some(builtin) = parse_builtin_ctor(name) {
return ResolvedCtor::Builtin(builtin);
}
if let Some((ctor_id, type_id, variant_name)) = ctx.resolve_user_ctor(name) {
return ResolvedCtor::User {
ctor_id,
type_id,
name: variant_name,
};
}
ResolvedCtor::Unresolved {
name: name.to_string(),
}
}
fn parse_builtin_ctor(name: &str) -> Option<BuiltinCtor> {
match name {
"Result.Ok" => Some(BuiltinCtor::ResultOk),
"Result.Err" => Some(BuiltinCtor::ResultErr),
"Option.Some" => Some(BuiltinCtor::OptionSome),
"Option.None" | "None" => Some(BuiltinCtor::OptionNone),
_ => None,
}
}
fn canonicalise_type(ctx: &ResolveCtx<'_>, ty: crate::ast::Type) -> crate::ast::Type {
use crate::ast::Type;
match ty {
Type::Named {
id: Some(existing),
name,
} => Type::Named {
id: Some(existing),
name,
},
Type::Named { id: None, name } => match ctx.resolve_type_id(&name) {
Some(id) => Type::Named { id: Some(id), name },
None => Type::Named { id: None, name },
},
Type::List(inner) => Type::List(Box::new(canonicalise_type(ctx, *inner))),
Type::Vector(inner) => Type::Vector(Box::new(canonicalise_type(ctx, *inner))),
Type::Option(inner) => Type::Option(Box::new(canonicalise_type(ctx, *inner))),
Type::Result(ok, err) => Type::Result(
Box::new(canonicalise_type(ctx, *ok)),
Box::new(canonicalise_type(ctx, *err)),
),
Type::Map(k, v) => Type::Map(
Box::new(canonicalise_type(ctx, *k)),
Box::new(canonicalise_type(ctx, *v)),
),
Type::Tuple(items) => Type::Tuple(
items
.into_iter()
.map(|t| canonicalise_type(ctx, t))
.collect(),
),
Type::Fn(params, ret, effects) => Type::Fn(
params
.into_iter()
.map(|t| canonicalise_type(ctx, t))
.collect(),
Box::new(canonicalise_type(ctx, *ret)),
effects,
),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinOp, Literal};
use crate::source::parse_source;
use crate::tco;
fn build(src: &str) -> (SymbolTable, Vec<TopLevel>) {
let mut items = parse_source(src).expect("parse failed");
tco::transform_program(&mut items);
let symbols = SymbolTable::build(&items, &[]);
(symbols, items)
}
fn first_fn(resolved: &[ResolvedTopLevel]) -> &ResolvedFnDef {
resolved
.iter()
.find_map(|t| match t {
ResolvedTopLevel::FnDef(f) => Some(f),
_ => None,
})
.expect("expected at least one resolved fn def")
}
#[test]
fn resolves_user_fn_call_to_fn_id() {
let (symbols, items) = build(
r#"
fn main() -> Int
helper(1)
fn helper(n: Int) -> Int
n + 1
"#,
);
let resolved = resolve_program(&symbols, &items);
let main_fn = resolved
.iter()
.find_map(|t| match t {
ResolvedTopLevel::FnDef(f) if f.name == "main" => Some(f),
_ => None,
})
.expect("main");
let stmts = main_fn.body.stmts();
let ResolvedStmt::Expr(call) = &stmts[0] else {
panic!("expected tail Expr stmt, got {:?}", stmts[0]);
};
match &call.node {
ResolvedExpr::Call(ResolvedCallee::Fn(_), args) => {
assert_eq!(args.len(), 1);
}
other => panic!("expected Call(Fn, _), got {:?}", other),
}
}
#[test]
fn resolves_builtin_namespace_method_call() {
let (symbols, items) = build(
r#"
fn add() -> Int
Int.abs(-3)
"#,
);
let resolved = resolve_program(&symbols, &items);
let f = first_fn(&resolved);
let ResolvedStmt::Expr(call) = &f.body.stmts()[0] else {
panic!("expected tail Expr");
};
match &call.node {
ResolvedExpr::Call(ResolvedCallee::Builtin(name), _) => {
assert_eq!(name, "Int.abs");
}
other => panic!("expected Call(Builtin, _), got {:?}", other),
}
}
#[test]
fn resolves_user_ctor_to_ctor_id_and_owning_type() {
let (symbols, items) = build(
r#"
type Shape
Circle(Float)
Square(Float)
fn make() -> Shape
Shape.Circle(1.0)
"#,
);
let resolved = resolve_program(&symbols, &items);
let f = first_fn(&resolved);
let ResolvedStmt::Expr(call) = &f.body.stmts()[0] else {
panic!("expected tail Expr");
};
match &call.node {
ResolvedExpr::Ctor(ResolvedCtor::User { type_id, name, .. }, args) => {
assert_eq!(name, "Circle");
assert!(*type_id != crate::ir::identity::TypeId(u32::MAX));
assert_eq!(args.len(), 1);
}
other => panic!("expected Ctor(User, _), got {:?}", other),
}
}
#[test]
fn resolves_builtin_ctor_result_ok() {
let (symbols, items) = build(
r#"
fn make() -> Result<Int, String>
Result.Ok(42)
"#,
);
let resolved = resolve_program(&symbols, &items);
let f = first_fn(&resolved);
let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
panic!("expected tail Expr");
};
match &expr.node {
ResolvedExpr::Ctor(ResolvedCtor::Builtin(BuiltinCtor::ResultOk), args) => {
assert_eq!(args.len(), 1);
}
other => panic!("expected Builtin(ResultOk), got {:?}", other),
}
}
#[test]
fn resolves_record_create_to_type_id() {
let (symbols, items) = build(
r#"
record Point
x: Int
y: Int
fn origin() -> Point
Point(x = 0, y = 0)
"#,
);
let resolved = resolve_program(&symbols, &items);
let f = first_fn(&resolved);
let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
panic!("expected tail Expr");
};
match &expr.node {
ResolvedExpr::RecordCreate {
type_id, type_name, ..
} => {
assert!(type_id.is_some(), "Point should resolve to a TypeId");
assert_eq!(type_name, "Point");
}
other => panic!("expected RecordCreate, got {:?}", other),
}
}
#[test]
fn resolves_tail_call_target_to_fn_id() {
let (symbols, items) = build(
r#"
fn count(n: Int, acc: Int) -> Int
match n
0 -> acc
_ -> count(n - 1, acc + 1)
"#,
);
let resolved = resolve_program(&symbols, &items);
let f = first_fn(&resolved);
let ResolvedStmt::Expr(top) = &f.body.stmts()[0] else {
panic!("expected tail Expr");
};
let ResolvedExpr::Match { arms, .. } = &top.node else {
panic!("expected match");
};
let recursive_body = &arms[1].body.node;
match recursive_body {
ResolvedExpr::TailCall { target, args } => {
assert_eq!(args.len(), 2);
let _ = target;
}
other => panic!("expected TailCall, got {:?}", other),
}
}
#[test]
fn resolves_binding_annotation_to_canonicalised_type() {
let (symbols, items) = build(
r#"
fn pair() -> Int
x: Int = 5
x
"#,
);
let resolved = resolve_program(&symbols, &items);
let f = first_fn(&resolved);
let stmts = f.body.stmts();
let ResolvedStmt::Binding { name, ty_ann, .. } = &stmts[0] else {
panic!("expected Binding stmt, got {:?}", stmts[0]);
};
assert_eq!(name, "x");
assert_eq!(ty_ann.as_ref(), Some(&crate::ast::Type::Int));
}
#[test]
fn binop_passes_through_structurally() {
let (symbols, items) = build(
r#"
fn add() -> Int
1 + 2
"#,
);
let resolved = resolve_program(&symbols, &items);
let f = first_fn(&resolved);
let ResolvedStmt::Expr(expr) = &f.body.stmts()[0] else {
panic!("tail expected");
};
match &expr.node {
ResolvedExpr::BinOp(BinOp::Add, l, r) => {
assert!(matches!(l.node, ResolvedExpr::Literal(Literal::Int(1))));
assert!(matches!(r.node, ResolvedExpr::Literal(Literal::Int(2))));
}
other => panic!("expected BinOp(Add, _, _), got {:?}", other),
}
}
#[test]
fn passthrough_for_verify_decision_typedef() {
let (symbols, items) = build(
r#"
type Tag
On
Off
verify alwaysTrue
1 => 1
fn alwaysTrue() -> Int
1
"#,
);
let resolved = resolve_program(&symbols, &items);
let mut saw_passthrough = false;
let mut saw_fn = false;
for item in &resolved {
match item {
ResolvedTopLevel::Passthrough(_) => saw_passthrough = true,
ResolvedTopLevel::FnDef(_) => saw_fn = true,
_ => {}
}
}
assert!(saw_passthrough, "expected at least one passthrough item");
assert!(saw_fn, "expected the FnDef to be promoted");
}
#[test]
fn ty_stamps_preserved_on_resolved_spans() {
use crate::ast::{Expr as AstExpr, Spanned as AstSpanned};
let lit = AstSpanned::new(AstExpr::Literal(Literal::Int(7)), 1);
let _ = lit.ty.set(crate::ast::Type::Int);
let symbols = SymbolTable::default();
let ctx = ResolveCtx::new(&symbols);
let resolved = resolve_spanned(&ctx, &lit);
assert_eq!(resolved.ty.get(), Some(&crate::ast::Type::Int));
}
}