use std::sync::Arc;
use crate::ast::{Expr, FnBody, FnDef, Spanned, Stmt, StrPart, TopLevel, Type};
pub fn annotate_program_alias_slots(items: &mut [TopLevel]) {
for item in items {
if let TopLevel::FnDef(fd) = item {
annotate_fn(fd);
}
}
}
fn annotate_fn(fd: &mut FnDef) {
let Some(res) = fd.resolution.clone() else {
return;
};
let local_count = res.local_count as usize;
let mut aliased = vec![false; local_count];
for (i, (_, ty)) in fd.params.iter().enumerate() {
if param_type_is_alias_prone(ty)
&& let Some(slot) = aliased.get_mut(i)
{
*slot = true;
}
}
let body = fd.body.clone();
let FnBody::Block(stmts) = body.as_ref();
for _ in 0..2 {
for stmt in stmts {
if let Stmt::Binding(name, _, expr) = stmt {
let Some(&slot) = res.local_slots.get(name) else {
continue;
};
flag_escaping_collection_locals(&expr.node, &res.local_slot_types, &mut aliased);
if slot_is_collection(slot, &res.local_slot_types)
&& !rhs_is_fresh_collection(expr)
&& let Some(s) = aliased.get_mut(slot as usize)
{
*s = true;
}
}
}
}
let new_res = crate::ast::FnResolution {
local_count: res.local_count,
local_slots: res.local_slots.clone(),
local_slot_types: res.local_slot_types.clone(),
aliased_slots: Arc::new(aliased),
};
fd.resolution = Some(new_res);
}
fn param_type_is_alias_prone(ty: &str) -> bool {
let trimmed = ty.trim();
trimmed.starts_with("Vector<") || trimmed.starts_with("Map<")
}
fn rhs_is_fresh_collection(expr: &Spanned<Expr>) -> bool {
match &expr.node {
Expr::MapLiteral(_) => true,
Expr::Match { arms, .. } => arms.iter().all(|a| rhs_is_fresh_collection(&a.body)),
Expr::FnCall(callee, args) => {
if is_option_with_default(&callee.node) && args.len() == 2 {
return self_keep_slot(&args[0].node, &args[1].node).is_some()
|| (rhs_is_fresh_collection(&args[0]) && rhs_is_fresh_collection(&args[1]));
}
is_fresh_collection_builtin(&callee.node, args)
}
_ => false,
}
}
fn is_fresh_collection_builtin(callee: &Expr, args: &[Spanned<Expr>]) -> bool {
let Expr::Attr(parent, member) = callee else {
return false;
};
let Expr::Ident(ns) = &parent.node else {
return false;
};
match (ns.as_str(), member.as_str()) {
("Vector", "set") | ("Vector", "fromList") | ("Map", "set") | ("Map", "remove") => true,
("Vector", "new") => args
.get(1)
.and_then(|a| a.ty())
.is_some_and(|t| !type_is_compound(&t.display())),
_ => false,
}
}
fn slot_is_collection(slot: u16, slot_types: &[Type]) -> bool {
slot_types
.get(slot as usize)
.is_some_and(|t| matches!(t, Type::Vector(_) | Type::Map(_, _)))
}
fn is_vector_map_builtin(callee: &Expr) -> bool {
matches!(callee, Expr::Attr(parent, _)
if matches!(&parent.node, Expr::Ident(p) if p == "Vector" || p == "Map"))
}
fn self_keep_slot(op1: &Expr, op2: &Expr) -> Option<u16> {
let Expr::Resolved { slot: kept, .. } = op2 else {
return None;
};
let Expr::FnCall(callee, set_args) = op1 else {
return None;
};
let Expr::Attr(parent, member) = &callee.node else {
return None;
};
let Expr::Ident(ns) = &parent.node else {
return None;
};
if !((ns == "Vector" || ns == "Map") && member == "set") {
return None;
}
match set_args.first().map(|a| &a.node) {
Some(Expr::Resolved { slot, .. }) if slot == kept => Some(*kept),
_ => None,
}
}
fn is_option_with_default(callee: &Expr) -> bool {
matches!(callee, Expr::Attr(parent, member)
if member == "withDefault"
&& matches!(&parent.node, Expr::Ident(p) if p == "Option"))
}
fn flag_escaping_collection_locals(expr: &Expr, slot_types: &[Type], aliased: &mut [bool]) {
match expr {
Expr::Resolved { slot, .. } => {
if slot_is_collection(*slot, slot_types)
&& let Some(s) = aliased.get_mut(*slot as usize)
{
*s = true;
}
}
Expr::FnCall(callee, args) => {
if is_option_with_default(&callee.node)
&& args.len() == 2
&& self_keep_slot(&args[0].node, &args[1].node).is_some()
{
if let Expr::FnCall(_, set_args) = &args[0].node {
for a in set_args.iter().skip(1) {
flag_escaping_collection_locals(&a.node, slot_types, aliased);
}
}
return;
}
let recv_skip = is_vector_map_builtin(&callee.node);
for (i, a) in args.iter().enumerate() {
if recv_skip && i == 0 && matches!(&a.node, Expr::Resolved { .. }) {
continue;
}
flag_escaping_collection_locals(&a.node, slot_types, aliased);
}
flag_escaping_collection_locals(&callee.node, slot_types, aliased);
}
Expr::Attr(inner, _) | Expr::Neg(inner) | Expr::ErrorProp(inner) => {
flag_escaping_collection_locals(&inner.node, slot_types, aliased);
}
Expr::BinOp(_, lhs, rhs) => {
flag_escaping_collection_locals(&lhs.node, slot_types, aliased);
flag_escaping_collection_locals(&rhs.node, slot_types, aliased);
}
Expr::Match { subject: _, arms } => {
for a in arms {
flag_escaping_collection_locals(&a.body.node, slot_types, aliased);
}
}
Expr::Constructor(_, payload) => {
if let Some(p) = payload {
flag_escaping_collection_locals(&p.node, slot_types, aliased);
}
}
Expr::Tuple(items) | Expr::List(items) | Expr::IndependentProduct(items, _) => {
for i in items {
flag_escaping_collection_locals(&i.node, slot_types, aliased);
}
}
Expr::MapLiteral(pairs) => {
for (k, v) in pairs {
flag_escaping_collection_locals(&k.node, slot_types, aliased);
flag_escaping_collection_locals(&v.node, slot_types, aliased);
}
}
Expr::RecordCreate { fields, .. } => {
for (_, e) in fields {
flag_escaping_collection_locals(&e.node, slot_types, aliased);
}
}
Expr::RecordUpdate { base, updates, .. } => {
flag_escaping_collection_locals(&base.node, slot_types, aliased);
for (_, e) in updates {
flag_escaping_collection_locals(&e.node, slot_types, aliased);
}
}
Expr::InterpolatedStr(parts) => {
for p in parts {
if let StrPart::Parsed(e) = p {
flag_escaping_collection_locals(&e.node, slot_types, aliased);
}
}
}
Expr::Literal(_) | Expr::Ident(_) | Expr::TailCall(_) => {}
}
}
fn type_is_compound(ty: &str) -> bool {
let trimmed = ty.trim();
trimmed.starts_with("Vector<")
|| trimmed.starts_with("Map<")
|| trimmed.starts_with("List<")
|| trimmed.starts_with("Tuple<")
|| trimmed.starts_with("Result<")
|| trimmed.starts_with("Option<")
|| (trimmed
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase())
&& !matches!(trimmed, "Int" | "Float" | "Bool" | "String" | "Unit"))
}