use std::collections::{HashMap, HashSet};
use bock_air::stubs::Value;
use bock_air::{AIRNode, AirInterpolationPart, NodeId, NodeKind};
use bock_ast::AssignOp;
use bock_errors::{DiagnosticBag, DiagnosticCode, Span};
const E_USE_AFTER_MOVE: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 5001,
};
const E_MUT_BORROW_NEEDS_MUT: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 5002,
};
const E_LOOP_MOVE: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 5003,
};
const E_MUT_RECEIVER_NEEDED: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 5004,
};
const MUT_LIST_RECEIVER_METHODS: &[&str] = &[
"push",
"append",
"pop",
"remove_at",
"insert",
"reverse",
"set",
];
pub type AIRModule = AIRNode;
#[derive(Debug, Clone, PartialEq)]
pub enum OwnershipState {
Owned,
Borrowed,
MutBorrowed,
Moved,
Managed,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OwnershipInfo {
pub state: OwnershipState,
pub mutable: bool,
pub origin: NodeId,
}
#[derive(Clone)]
struct VarOwnership {
state: OwnershipState,
is_mut: bool,
move_site: Option<Span>,
}
struct OwnershipAnalyzer {
diags: DiagnosticBag,
env: HashMap<String, VarOwnership>,
in_loop: bool,
loop_entry_keys: HashSet<String>,
in_managed: bool,
}
impl OwnershipAnalyzer {
fn new() -> Self {
Self {
diags: DiagnosticBag::new(),
env: HashMap::new(),
in_loop: false,
loop_entry_keys: HashSet::new(),
in_managed: false,
}
}
fn snapshot(&self) -> HashMap<String, VarOwnership> {
self.env.clone()
}
fn check_use(&mut self, name: &str, use_span: Span) {
if let Some(var) = self.env.get(name) {
if matches!(var.state, OwnershipState::Moved) {
let move_site = var.move_site;
let diag = self.diags.error(
E_USE_AFTER_MOVE,
format!("use of moved variable `{name}`"),
use_span,
);
if let Some(ms) = move_site {
diag.label(ms, "value moved here");
}
}
}
}
fn do_move(&mut self, name: &str, move_span: Span) {
if let Some(var) = self.env.get(name) {
if matches!(var.state, OwnershipState::Managed) {
return; }
}
if self.in_loop && self.loop_entry_keys.contains(name) {
self.diags.error(
E_LOOP_MOVE,
format!(
"cannot move `{name}` inside a loop \
(would be moved on every iteration)"
),
move_span,
);
}
if let Some(var) = self.env.get_mut(name) {
if matches!(var.state, OwnershipState::Moved) {
let move_site = var.move_site;
let diag = self.diags.error(
E_USE_AFTER_MOVE,
format!("use of moved variable `{name}`"),
move_span,
);
if let Some(ms) = move_site {
diag.label(ms, "value moved here");
}
} else {
var.state = OwnershipState::Moved;
var.move_site = Some(move_span);
}
}
}
fn analyze_move(&mut self, node: &AIRNode) -> bool {
if let NodeKind::Identifier { name } = &node.kind {
if let Some(var) = self.env.get(&name.name) {
if matches!(var.state, OwnershipState::Managed) {
return false; }
}
if node.metadata.get("copy_type") == Some(&Value::Bool(true)) {
return false;
}
self.do_move(&name.name, node.span);
false
} else {
self.analyze_node(node)
}
}
fn is_mutable_lvalue(&self, node: &AIRNode) -> bool {
match &node.kind {
NodeKind::Identifier { name } => match self.env.get(&name.name) {
Some(var) => var.is_mut || matches!(var.state, OwnershipState::Managed),
None => false,
},
NodeKind::FieldAccess { object, .. } => self.is_mutable_lvalue(object),
NodeKind::Index { object, .. } => self.is_mutable_lvalue(object),
_ => false,
}
}
fn check_list_mut_receiver(&mut self, call: &AIRNode, callee: &AIRNode) {
if self.recv_kind(call) != Some("List") {
return;
}
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return;
};
if !MUT_LIST_RECEIVER_METHODS.contains(&field.name.as_str()) {
return;
}
if self.is_mutable_lvalue(object) {
return;
}
let recv_desc = match &object.kind {
NodeKind::Identifier { name } => format!("`{}`", name.name),
_ => "the receiver".to_string(),
};
self.diags
.error(
E_MUT_RECEIVER_NEEDED,
format!(
"cannot call `{}` on {recv_desc}: \
it mutates the list in place and requires a `mut` receiver",
field.name
),
object.span,
)
.note(
"declare the list with `let mut`, or use `+` / `concat` to build a \
new list without mutation",
);
}
fn recv_kind<'a>(&self, node: &'a AIRNode) -> Option<&'a str> {
match node.metadata.get(crate::checker::RECV_KIND_META_KEY) {
Some(Value::String(s)) => Some(s.as_str()),
_ => None,
}
}
fn merge_states(
&self,
pre: &HashMap<String, VarOwnership>,
branches: &[(bool, HashMap<String, VarOwnership>)],
) -> HashMap<String, VarOwnership> {
let non_div: Vec<&HashMap<String, VarOwnership>> = branches
.iter()
.filter(|(div, _)| !*div)
.map(|(_, s)| s)
.collect();
if non_div.is_empty() {
return pre.clone();
}
let mut result = pre.clone();
for name in pre.keys() {
let any_moved = non_div.iter().any(|state| {
state
.get(name)
.is_some_and(|v| matches!(v.state, OwnershipState::Moved))
});
if any_moved {
if let Some(var) = result.get_mut(name) {
let move_site = non_div
.iter()
.filter_map(|state| state.get(name))
.find(|v| matches!(v.state, OwnershipState::Moved))
.and_then(|v| v.move_site);
var.state = OwnershipState::Moved;
var.move_site = move_site;
}
}
}
result
}
fn bind_param(&mut self, param: &AIRNode) {
if let NodeKind::Param { pattern, .. } = ¶m.kind {
if let NodeKind::BindPat { name, is_mut } = &pattern.kind {
let state = if self.in_managed {
OwnershipState::Managed
} else {
OwnershipState::Owned
};
self.env.insert(
name.name.clone(),
VarOwnership {
state,
is_mut: *is_mut,
move_site: None,
},
);
}
}
}
fn bind_pattern(&mut self, pat: &AIRNode) {
let base_state = if self.in_managed {
OwnershipState::Managed
} else {
OwnershipState::Owned
};
match &pat.kind {
NodeKind::BindPat { name, is_mut } => {
self.env.insert(
name.name.clone(),
VarOwnership {
state: base_state,
is_mut: *is_mut,
move_site: None,
},
);
}
NodeKind::TuplePat { elems } => {
for e in elems {
self.bind_pattern(e);
}
}
NodeKind::ConstructorPat { fields, .. } => {
for f in fields {
self.bind_pattern(f);
}
}
NodeKind::RecordPat { fields, .. } => {
for f in fields {
if let Some(p) = &f.pattern {
self.bind_pattern(p);
} else {
self.env.insert(
f.name.name.clone(),
VarOwnership {
state: base_state.clone(),
is_mut: false,
move_site: None,
},
);
}
}
}
NodeKind::ListPat { elems, rest } => {
for e in elems {
self.bind_pattern(e);
}
if let Some(r) = rest {
self.bind_pattern(r);
}
}
NodeKind::OrPat { alternatives } => {
if let Some(first) = alternatives.first() {
self.bind_pattern(first);
}
}
_ => {}
}
}
#[allow(clippy::too_many_lines)]
fn analyze_node(&mut self, node: &AIRNode) -> bool {
match &node.kind {
NodeKind::Module { imports, items, .. } => {
for n in imports {
self.analyze_node(n);
}
for n in items {
self.analyze_node(n);
}
false
}
NodeKind::FnDecl {
annotations,
params,
body,
..
} => {
let outer = self.snapshot();
let outer_managed = self.in_managed;
if annotations.iter().any(|a| a.name.name == "managed") {
self.in_managed = true;
}
for p in params {
self.bind_param(p);
}
self.analyze_node(body);
self.env = outer;
self.in_managed = outer_managed;
false
}
NodeKind::ImplBlock { methods, .. } => {
for m in methods {
self.analyze_node(m);
}
false
}
NodeKind::ClassDecl { methods, .. } => {
for m in methods {
self.analyze_node(m);
}
false
}
NodeKind::TraitDecl { methods, .. } => {
for m in methods {
self.analyze_node(m);
}
false
}
NodeKind::RecordDecl { .. }
| NodeKind::EnumDecl { .. }
| NodeKind::TypeAlias { .. }
| NodeKind::EffectDecl { .. }
| NodeKind::ConstDecl { .. }
| NodeKind::ImportDecl { .. }
| NodeKind::ModuleHandle { .. }
| NodeKind::PropertyTest { .. } => false,
NodeKind::Block { stmts, tail } => {
let pre_keys: HashSet<String> = self.env.keys().cloned().collect();
let mut diverges = false;
for stmt in stmts {
if diverges {
break;
}
diverges = self.analyze_node(stmt);
}
if !diverges {
if let Some(t) = tail {
diverges = self.analyze_node(t);
}
}
self.env.retain(|k, _| pre_keys.contains(k));
diverges
}
NodeKind::LetBinding {
is_mut,
pattern,
value,
..
} => {
let is_managed =
self.in_managed || node.metadata.get("managed") == Some(&Value::Bool(true));
self.analyze_move(value);
let state = if is_managed {
OwnershipState::Managed
} else {
OwnershipState::Owned
};
let own = VarOwnership {
state,
is_mut: *is_mut,
move_site: None,
};
if is_managed {
if let NodeKind::BindPat { name, .. } = &pattern.kind {
self.env.insert(name.name.clone(), own);
} else {
self.bind_pattern(pattern);
}
} else {
self.bind_pattern(pattern);
}
false
}
NodeKind::Assign { op, target, value } => {
match op {
AssignOp::Assign => {
self.analyze_move(value);
self.analyze_node(target);
}
_ => {
self.analyze_node(target);
self.analyze_node(value);
}
}
false
}
NodeKind::Identifier { name } => {
self.check_use(&name.name, node.span);
false
}
NodeKind::Move { expr } => {
self.analyze_move(expr);
false
}
NodeKind::Borrow { expr } => {
self.analyze_node(expr);
false
}
NodeKind::MutableBorrow { expr } => {
if let NodeKind::Identifier { name } = &expr.kind {
if let Some(var) = self.env.get(&name.name) {
if !var.is_mut && !matches!(var.state, OwnershipState::Managed) {
self.diags.error(
E_MUT_BORROW_NEEDS_MUT,
format!(
"cannot mutably borrow `{}`: \
variable not declared `mut`",
name.name
),
expr.span,
);
}
}
}
self.analyze_node(expr);
false
}
NodeKind::Return { value } => {
if let Some(v) = value {
let old_in_loop = self.in_loop;
self.in_loop = false;
self.analyze_move(v);
self.in_loop = old_in_loop;
}
true
}
NodeKind::Break { value } => {
if let Some(v) = value {
let old_in_loop = self.in_loop;
self.in_loop = false;
self.analyze_move(v);
self.in_loop = old_in_loop;
}
true
}
NodeKind::Continue => true,
NodeKind::Unreachable => true,
NodeKind::If {
condition,
then_block,
else_block,
..
} => {
self.analyze_node(condition);
let pre = self.snapshot();
let then_div = self.analyze_node(then_block);
let then_state = self.snapshot();
self.env = pre.clone();
let (else_div, else_state) = match else_block {
Some(eb) => {
let d = self.analyze_node(eb);
(d, self.snapshot())
}
None => {
(false, pre.clone())
}
};
self.env =
self.merge_states(&pre, &[(then_div, then_state), (else_div, else_state)]);
then_div && else_div
}
NodeKind::Guard {
let_pattern,
condition,
else_block,
} => {
if let Some(pat) = let_pattern {
self.analyze_node(pat);
}
self.analyze_node(condition);
let pre = self.snapshot();
self.analyze_node(else_block);
self.env = pre;
false
}
NodeKind::Match { scrutinee, arms } => {
self.analyze_node(scrutinee);
let pre = self.snapshot();
let mut arm_results: Vec<(bool, HashMap<String, VarOwnership>)> =
Vec::with_capacity(arms.len());
for arm in arms {
self.env = pre.clone();
let div = self.analyze_node(arm);
arm_results.push((div, self.snapshot()));
}
self.env = self.merge_states(&pre, &arm_results);
arm_results.iter().all(|(d, _)| *d)
}
NodeKind::MatchArm {
pattern,
guard,
body,
} => {
let pre_keys: HashSet<String> = self.env.keys().cloned().collect();
self.bind_pattern(pattern);
if let Some(g) = guard {
self.analyze_node(g);
}
let div = self.analyze_node(body);
self.env.retain(|k, _| pre_keys.contains(k));
div
}
NodeKind::For {
pattern,
iterable,
body,
} => {
self.analyze_node(iterable);
let pre = self.snapshot();
let old_in_loop = self.in_loop;
let old_loop_keys = std::mem::take(&mut self.loop_entry_keys);
self.in_loop = true;
self.loop_entry_keys = pre.keys().cloned().collect();
self.bind_pattern(pattern);
self.analyze_node(body);
self.in_loop = old_in_loop;
self.loop_entry_keys = old_loop_keys;
self.env = pre;
false
}
NodeKind::While { condition, body } => {
self.analyze_node(condition);
let pre = self.snapshot();
let old_in_loop = self.in_loop;
let old_loop_keys = std::mem::take(&mut self.loop_entry_keys);
self.in_loop = true;
self.loop_entry_keys = pre.keys().cloned().collect();
self.analyze_node(body);
self.in_loop = old_in_loop;
self.loop_entry_keys = old_loop_keys;
self.env = pre;
false
}
NodeKind::Loop { body } => {
let pre = self.snapshot();
let old_in_loop = self.in_loop;
let old_loop_keys = std::mem::take(&mut self.loop_entry_keys);
self.in_loop = true;
self.loop_entry_keys = pre.keys().cloned().collect();
self.analyze_node(body);
self.in_loop = old_in_loop;
self.loop_entry_keys = old_loop_keys;
self.env = pre;
false
}
NodeKind::Call { callee, args, .. } => {
self.check_list_mut_receiver(node, callee);
self.analyze_node(callee);
for arg in args {
self.analyze_node(&arg.value);
}
false
}
NodeKind::MethodCall { receiver, args, .. } => {
self.analyze_node(receiver);
for arg in args {
self.analyze_node(&arg.value);
}
false
}
NodeKind::Lambda { params, body } => {
let outer = self.snapshot();
for p in params {
self.bind_param(p);
}
self.analyze_node(body);
self.env = outer;
false
}
NodeKind::BinaryOp { left, right, .. } => {
self.analyze_node(left);
self.analyze_node(right);
false
}
NodeKind::UnaryOp { operand, .. } => {
self.analyze_node(operand);
false
}
NodeKind::FieldAccess { object, .. } => {
self.analyze_node(object);
false
}
NodeKind::Index { object, index } => {
self.analyze_node(object);
self.analyze_node(index);
false
}
NodeKind::Propagate { expr } => {
self.analyze_node(expr);
false
}
NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
self.analyze_node(left);
self.analyze_node(right);
false
}
NodeKind::Await { expr } => {
self.analyze_node(expr);
false
}
NodeKind::Range { lo, hi, .. } => {
self.analyze_node(lo);
self.analyze_node(hi);
false
}
NodeKind::RecordConstruct { fields, spread, .. } => {
for f in fields {
if let Some(v) = &f.value {
self.analyze_move(v);
}
}
if let Some(s) = spread {
self.analyze_node(s);
}
false
}
NodeKind::ListLiteral { elems }
| NodeKind::SetLiteral { elems }
| NodeKind::TupleLiteral { elems } => {
for e in elems {
self.analyze_move(e);
}
false
}
NodeKind::MapLiteral { entries } => {
for entry in entries {
self.analyze_move(&entry.key);
self.analyze_move(&entry.value);
}
false
}
NodeKind::Interpolation { parts } => {
for part in parts {
if let AirInterpolationPart::Expr(e) = part {
self.analyze_node(e);
}
}
false
}
NodeKind::ResultConstruct { value, .. } => {
if let Some(v) = value {
self.analyze_move(v);
}
false
}
NodeKind::HandlingBlock { handlers, body } => {
for h in handlers {
self.analyze_node(&h.handler);
}
self.analyze_node(body);
false
}
NodeKind::Literal { .. }
| NodeKind::Placeholder
| NodeKind::TypeSelf
| NodeKind::WildcardPat
| NodeKind::LiteralPat { .. }
| NodeKind::RestPat
| NodeKind::TypeNamed { .. }
| NodeKind::TypeTuple { .. }
| NodeKind::TypeFunction { .. }
| NodeKind::TypeOptional { .. }
| NodeKind::EffectOp { .. }
| NodeKind::EffectRef { .. }
| NodeKind::Error => false,
NodeKind::BindPat { .. }
| NodeKind::ConstructorPat { .. }
| NodeKind::RecordPat { .. }
| NodeKind::TuplePat { .. }
| NodeKind::ListPat { .. }
| NodeKind::OrPat { .. }
| NodeKind::GuardPat { .. }
| NodeKind::RangePat { .. } => false,
_ => false,
}
}
}
#[must_use]
pub fn analyze_ownership(module: &AIRModule) -> DiagnosticBag {
let mut analyzer = OwnershipAnalyzer::new();
analyzer.analyze_node(module);
analyzer.diags
}
#[cfg(test)]
mod tests {
use super::*;
use bock_air::stubs::Value;
use bock_air::{AIRNode, AirArg, NodeIdGen, NodeKind};
use bock_ast::{Ident, Literal};
use bock_errors::{FileId, Span};
fn span() -> Span {
Span {
file: FileId(0),
start: 0,
end: 0,
}
}
fn span_at(start: usize, end: usize) -> Span {
Span {
file: FileId(0),
start,
end,
}
}
fn ident(name: &str) -> Ident {
Ident {
name: name.to_string(),
span: span(),
}
}
fn node(gen: &NodeIdGen, kind: NodeKind) -> AIRNode {
AIRNode::new(gen.next(), span(), kind)
}
fn node_at(gen: &NodeIdGen, kind: NodeKind, s: usize, e: usize) -> AIRNode {
AIRNode::new(gen.next(), span_at(s, e), kind)
}
fn id_node(gen: &NodeIdGen, name: &str) -> AIRNode {
node(gen, NodeKind::Identifier { name: ident(name) })
}
fn id_node_at(gen: &NodeIdGen, name: &str, s: usize, e: usize) -> AIRNode {
node_at(gen, NodeKind::Identifier { name: ident(name) }, s, e)
}
fn lit_node(gen: &NodeIdGen) -> AIRNode {
node(
gen,
NodeKind::Literal {
lit: Literal::Int("42".into()),
},
)
}
fn bind_pat(gen: &NodeIdGen, name: &str, is_mut: bool) -> AIRNode {
node(
gen,
NodeKind::BindPat {
name: ident(name),
is_mut,
},
)
}
fn let_binding(gen: &NodeIdGen, name: &str, is_mut: bool, value: AIRNode) -> AIRNode {
node(
gen,
NodeKind::LetBinding {
is_mut,
pattern: Box::new(bind_pat(gen, name, is_mut)),
ty: None,
value: Box::new(value),
},
)
}
fn block(gen: &NodeIdGen, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
node(
gen,
NodeKind::Block {
stmts,
tail: tail.map(Box::new),
},
)
}
fn module(gen: &NodeIdGen, items: Vec<AIRNode>) -> AIRNode {
node(
gen,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items,
},
)
}
fn fn_decl(gen: &NodeIdGen, body: AIRNode) -> AIRNode {
fn_decl_with(gen, body, vec![])
}
fn managed_fn_decl(gen: &NodeIdGen, body: AIRNode) -> AIRNode {
use bock_ast::Annotation;
fn_decl_with(
gen,
body,
vec![Annotation {
id: 0,
span: span(),
name: ident("managed"),
args: vec![],
}],
)
}
fn fn_decl_with(
gen: &NodeIdGen,
body: AIRNode,
annotations: Vec<bock_ast::Annotation>,
) -> AIRNode {
node(
gen,
NodeKind::FnDecl {
annotations,
visibility: bock_ast::Visibility::Public,
is_async: false,
name: ident("f"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
)
}
fn return_node(gen: &NodeIdGen, val: Option<AIRNode>) -> AIRNode {
node(
gen,
NodeKind::Return {
value: val.map(Box::new),
},
)
}
fn move_node(gen: &NodeIdGen, expr: AIRNode) -> AIRNode {
node(
gen,
NodeKind::Move {
expr: Box::new(expr),
},
)
}
fn mut_borrow(gen: &NodeIdGen, expr: AIRNode) -> AIRNode {
node(
gen,
NodeKind::MutableBorrow {
expr: Box::new(expr),
},
)
}
fn if_node(gen: &NodeIdGen, cond: AIRNode, then: AIRNode, else_: Option<AIRNode>) -> AIRNode {
node(
gen,
NodeKind::If {
let_pattern: None,
condition: Box::new(cond),
then_block: Box::new(then),
else_block: else_.map(Box::new),
},
)
}
fn guard_node(gen: &NodeIdGen, cond: AIRNode, else_block: AIRNode) -> AIRNode {
node(
gen,
NodeKind::Guard {
let_pattern: None,
condition: Box::new(cond),
else_block: Box::new(else_block),
},
)
}
fn loop_node(gen: &NodeIdGen, body: AIRNode) -> AIRNode {
node(
gen,
NodeKind::Loop {
body: Box::new(body),
},
)
}
fn match_node(gen: &NodeIdGen, scrutinee: AIRNode, arms: Vec<AIRNode>) -> AIRNode {
node(
gen,
NodeKind::Match {
scrutinee: Box::new(scrutinee),
arms,
},
)
}
fn match_arm(gen: &NodeIdGen, pat: AIRNode, body: AIRNode) -> AIRNode {
node(
gen,
NodeKind::MatchArm {
pattern: Box::new(pat),
guard: None,
body: Box::new(body),
},
)
}
#[test]
fn no_error_simple_borrow() {
let gen = NodeIdGen::new();
let data_lit = lit_node(&gen);
let let_data = let_binding(&gen, "data", false, data_lit);
let use1 = id_node(&gen, "data");
let use2 = id_node(&gen, "data");
let call = node(
&gen,
NodeKind::Call {
callee: Box::new(id_node(&gen, "summarize")),
args: vec![AirArg {
label: None,
value: use1,
}],
type_args: vec![],
},
);
let b = block(&gen, vec![let_data, call, use2], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(
!diags.has_errors(),
"expected no errors, got: {:?}",
diags.iter().collect::<Vec<_>>()
);
}
#[test]
fn move_on_let_binding() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let id_data = id_node_at(&gen, "data", 10, 14);
let let_archive = let_binding(&gen, "archive", false, id_data);
let use_data = id_node_at(&gen, "data", 20, 24);
let b = block(&gen, vec![let_data, let_archive, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
}
#[test]
fn explicit_move_node() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let mv = move_node(&gen, id_node_at(&gen, "data", 5, 9));
let use_data = id_node_at(&gen, "data", 15, 19);
let b = block(&gen, vec![let_data, mv, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
}
#[test]
fn use_after_move_has_move_site_label() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let mv = move_node(&gen, id_node_at(&gen, "data", 5, 9));
let use_data = id_node_at(&gen, "data", 15, 19);
let b = block(&gen, vec![let_data, mv, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
let err = diags.iter().find(|d| d.code == E_USE_AFTER_MOVE).unwrap();
assert!(
!err.labels.is_empty(),
"expected a label pointing to move site"
);
assert!(err.labels[0].message.contains("moved"));
}
#[test]
fn mut_borrow_of_non_mut_errors() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let mb = mut_borrow(&gen, id_node(&gen, "data"));
let b = block(&gen, vec![let_data, mb], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_MUT_BORROW_NEEDS_MUT));
}
#[test]
fn mut_borrow_of_mut_ok() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", true, lit_node(&gen));
let mb = mut_borrow(&gen, id_node(&gen, "data"));
let b = block(&gen, vec![let_data, mb], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn managed_skips_ownership_tracking() {
let gen = NodeIdGen::new();
let mut let_data = let_binding(&gen, "data", false, lit_node(&gen));
let_data
.metadata
.insert("managed".into(), Value::Bool(true));
let id_data = id_node(&gen, "data");
let let_archive = let_binding(&gen, "archive", false, id_data);
let use_data = id_node(&gen, "data");
let b = block(&gen, vec![let_data, let_archive, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn managed_fn_suppresses_move_errors() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let id_data1 = id_node(&gen, "data");
let let_a = let_binding(&gen, "a", false, id_data1);
let id_data2 = id_node(&gen, "data");
let let_b = let_binding(&gen, "b", false, id_data2);
let b = block(&gen, vec![let_data, let_a, let_b], None);
let m = module(&gen, vec![managed_fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(
!diags.has_errors(),
"expected no errors in @managed fn, got: {:?}",
diags.iter().collect::<Vec<_>>()
);
}
#[test]
fn non_managed_fn_still_errors_on_reuse() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let id_data1 = id_node(&gen, "data");
let let_a = let_binding(&gen, "a", false, id_data1);
let id_data2 = id_node_at(&gen, "data", 20, 24);
let let_b = let_binding(&gen, "b", false, id_data2);
let b = block(&gen, vec![let_data, let_a, let_b], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
}
#[test]
fn managed_fn_loop_move_suppressed() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let id_data = id_node(&gen, "data");
let let_discard = let_binding(&gen, "_d", false, id_data);
let loop_body = block(&gen, vec![let_discard], None);
let lp = loop_node(&gen, loop_body);
let b = block(&gen, vec![let_data, lp], None);
let m = module(&gen, vec![managed_fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(
!diags.has_errors(),
"expected no errors in @managed fn loop, got: {:?}",
diags.iter().collect::<Vec<_>>()
);
}
#[test]
fn guard_else_diverges_data_still_owned() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let cond = lit_node(&gen);
let else_block = block(&gen, vec![return_node(&gen, None)], None);
let guard = guard_node(&gen, cond, else_block);
let use_data = id_node(&gen, "data");
let b = block(&gen, vec![let_data, guard, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn if_with_diverging_then_data_still_owned() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let cond = lit_node(&gen);
let id_data = id_node(&gen, "data");
let let_archive = let_binding(&gen, "archive", false, id_data);
let ret = return_node(&gen, None);
let then_block = block(&gen, vec![let_archive, ret], None);
let if_node_ = if_node(&gen, cond, then_block, None);
let use_data = id_node(&gen, "data");
let b = block(&gen, vec![let_data, if_node_, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn if_non_diverging_branch_moves_makes_moved_at_join() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let cond = lit_node(&gen);
let id_data = id_node(&gen, "data");
let let_archive = let_binding(&gen, "archive", false, id_data);
let then_block = block(&gen, vec![let_archive], None);
let if_node_ = if_node(&gen, cond, then_block, None);
let use_data = id_node_at(&gen, "data", 30, 34);
let b = block(&gen, vec![let_data, if_node_, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
}
#[test]
fn if_both_branches_diverge_join_uses_pre_state() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let cond = lit_node(&gen);
let then_block = block(&gen, vec![return_node(&gen, None)], None);
let else_block = block(&gen, vec![return_node(&gen, None)], None);
let if_node_ = if_node(&gen, cond, then_block, Some(else_block));
let b = block(&gen, vec![let_data, if_node_], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn move_inside_loop_is_error() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let id_data = id_node_at(&gen, "data", 10, 14);
let let_archive = let_binding(&gen, "archive", false, id_data);
let loop_body = block(&gen, vec![let_archive], None);
let lp = loop_node(&gen, loop_body);
let b = block(&gen, vec![let_data, lp], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_LOOP_MOVE));
}
#[test]
fn variable_defined_inside_loop_can_be_moved() {
let gen = NodeIdGen::new();
let let_tmp = let_binding(&gen, "tmp", false, lit_node(&gen));
let id_tmp = id_node(&gen, "tmp");
let let_discard = let_binding(&gen, "_unused", false, id_tmp);
let loop_body = block(&gen, vec![let_tmp, let_discard], None);
let lp = loop_node(&gen, loop_body);
let b = block(&gen, vec![lp], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn match_all_arms_diverge_no_use_after_move() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let scrutinee = lit_node(&gen);
let id_data = id_node(&gen, "data");
let let_discard = let_binding(&gen, "_d", false, id_data);
let ret1 = return_node(&gen, None);
let arm1_body = block(&gen, vec![let_discard, ret1], None);
let arm1 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm1_body);
let ret2 = return_node(&gen, None);
let arm2_body = block(&gen, vec![ret2], None);
let arm2 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm2_body);
let m_node = match_node(&gen, scrutinee, vec![arm1, arm2]);
let b = block(&gen, vec![let_data, m_node], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn match_non_diverging_arm_moves_is_error_after() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let scrutinee = lit_node(&gen);
let id_data = id_node(&gen, "data");
let let_discard = let_binding(&gen, "_d", false, id_data);
let arm1_body = block(&gen, vec![let_discard], None);
let arm1 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm1_body);
let ret = return_node(&gen, None);
let arm2_body = block(&gen, vec![ret], None);
let arm2 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm2_body);
let m_node = match_node(&gen, scrutinee, vec![arm1, arm2]);
let use_data = id_node_at(&gen, "data", 50, 54);
let b = block(&gen, vec![let_data, m_node, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
}
#[test]
fn if_inside_match_arm_nested() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let scrutinee = lit_node(&gen);
let cond = lit_node(&gen);
let id_data = id_node(&gen, "data");
let let_discard = let_binding(&gen, "_d", false, id_data);
let then_block = block(&gen, vec![let_discard], None);
let else_block = block(&gen, vec![return_node(&gen, None)], None);
let if_expr = if_node(&gen, cond, then_block, Some(else_block));
let arm_body = block(&gen, vec![if_expr], None);
let arm = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm_body);
let m_node = match_node(&gen, scrutinee, vec![arm]);
let use_data = id_node_at(&gen, "data", 50, 54);
let b = block(&gen, vec![let_data, m_node, use_data], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
}
#[test]
fn no_false_positive_if_else_both_leave_owned() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let cond = lit_node(&gen);
let use1 = id_node(&gen, "data");
let use2 = id_node(&gen, "data");
let then_block = block(&gen, vec![use1], None);
let else_block = block(&gen, vec![use2], None);
let if_expr = if_node(&gen, cond, then_block, Some(else_block));
let use_after = id_node(&gen, "data");
let b = block(&gen, vec![let_data, if_expr, use_after], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(!diags.has_errors());
}
#[test]
fn double_move_error() {
let gen = NodeIdGen::new();
let let_data = let_binding(&gen, "data", false, lit_node(&gen));
let let_a = let_binding(&gen, "a", false, id_node(&gen, "data"));
let let_b = let_binding(&gen, "b", false, id_node_at(&gen, "data", 20, 24));
let b = block(&gen, vec![let_data, let_a, let_b], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.has_errors());
assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
}
fn desugared_list_call(
gen: &NodeIdGen,
receiver: AIRNode,
method: &str,
arg: AIRNode,
) -> AIRNode {
let recv_self = receiver.clone();
let callee = node(
gen,
NodeKind::FieldAccess {
object: Box::new(receiver),
field: ident(method),
},
);
let mut call = node(
gen,
NodeKind::Call {
callee: Box::new(callee),
args: vec![
AirArg {
label: None,
value: recv_self,
},
AirArg {
label: None,
value: arg,
},
],
type_args: vec![],
},
);
call.metadata.insert(
crate::checker::RECV_KIND_META_KEY.to_string(),
Value::String("List".to_string()),
);
call
}
#[test]
fn push_on_mut_let_binding_ok() {
let gen = NodeIdGen::new();
let let_acc = let_binding(&gen, "acc", true, lit_node(&gen));
let push = desugared_list_call(&gen, id_node(&gen, "acc"), "push", lit_node(&gen));
let b = block(&gen, vec![let_acc, push], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(
!diags.iter().any(|d| d.code == E_MUT_RECEIVER_NEEDED),
"push on a `mut` binding must not error: {:?}",
diags.iter().collect::<Vec<_>>()
);
}
#[test]
fn push_on_non_mut_let_binding_errors() {
let gen = NodeIdGen::new();
let let_acc = let_binding(&gen, "acc", false, lit_node(&gen));
let push = desugared_list_call(
&gen,
id_node_at(&gen, "acc", 10, 13),
"push",
lit_node(&gen),
);
let b = block(&gen, vec![let_acc, push], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.iter().any(|d| d.code == E_MUT_RECEIVER_NEEDED));
let err = diags
.iter()
.find(|d| d.code == E_MUT_RECEIVER_NEEDED)
.unwrap();
assert!(err.message.contains("push"));
assert!(err.message.contains("acc"));
assert!(
!err.notes.is_empty(),
"expected a `let mut` suggestion note"
);
}
#[test]
fn append_on_non_mut_let_binding_errors() {
let gen = NodeIdGen::new();
let let_acc = let_binding(&gen, "acc", false, lit_node(&gen));
let push = desugared_list_call(&gen, id_node(&gen, "acc"), "append", lit_node(&gen));
let b = block(&gen, vec![let_acc, push], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(diags.iter().any(|d| d.code == E_MUT_RECEIVER_NEEDED));
}
#[test]
fn push_on_mut_parameter_ok() {
let gen = NodeIdGen::new();
let push = desugared_list_call(&gen, id_node(&gen, "xs"), "push", lit_node(&gen));
let b = block(&gen, vec![push], None);
let param = node(
&gen,
NodeKind::Param {
pattern: Box::new(bind_pat(&gen, "xs", true)),
ty: None,
default: None,
},
);
let f = node(
&gen,
NodeKind::FnDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
is_async: false,
name: ident("f"),
generic_params: vec![],
params: vec![param],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(b),
},
);
let m = module(&gen, vec![f]);
let diags = analyze_ownership(&m);
assert!(
!diags.iter().any(|d| d.code == E_MUT_RECEIVER_NEEDED),
"push on a `mut` parameter must not error"
);
}
#[test]
fn dq30_mutators_on_non_mut_let_binding_error() {
for method in ["pop", "remove_at", "insert", "reverse", "set"] {
let gen = NodeIdGen::new();
let let_acc = let_binding(&gen, "acc", false, lit_node(&gen));
let call = desugared_list_call(&gen, id_node(&gen, "acc"), method, lit_node(&gen));
let b = block(&gen, vec![let_acc, call], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(
diags.iter().any(|d| d.code == E_MUT_RECEIVER_NEEDED),
"`{method}` on a non-`mut` binding must emit E5004"
);
}
}
#[test]
fn dq30_mutators_on_mut_let_binding_ok() {
for method in ["pop", "remove_at", "insert", "reverse", "set"] {
let gen = NodeIdGen::new();
let let_acc = let_binding(&gen, "acc", true, lit_node(&gen));
let call = desugared_list_call(&gen, id_node(&gen, "acc"), method, lit_node(&gen));
let b = block(&gen, vec![let_acc, call], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(
!diags.iter().any(|d| d.code == E_MUT_RECEIVER_NEEDED),
"`{method}` on a `mut` binding must not emit E5004: {:?}",
diags.iter().collect::<Vec<_>>()
);
}
}
#[test]
fn push_in_loop_on_mut_binding_ok() {
let gen = NodeIdGen::new();
let let_acc = let_binding(&gen, "acc", true, lit_node(&gen));
let push = desugared_list_call(&gen, id_node(&gen, "acc"), "push", lit_node(&gen));
let loop_body = block(&gen, vec![push], None);
let lp = loop_node(&gen, loop_body);
let b = block(&gen, vec![let_acc, lp], None);
let m = module(&gen, vec![fn_decl(&gen, b)]);
let diags = analyze_ownership(&m);
assert!(
!diags.iter().any(|d| d.code == E_MUT_RECEIVER_NEEDED),
"push in a loop on a `mut` binding must not error: {:?}",
diags.iter().collect::<Vec<_>>()
);
}
}