Skip to main content

logicaffeine_compile/analysis/
ownership.rs

1//! Native ownership analysis for use-after-move detection.
2//!
3//! Lightweight data-flow analysis that catches the 90% common ownership errors
4//! at check-time (milliseconds), before Rust compilation. This pass tracks
5//! `Owned`, `Moved`, and `Borrowed` states through control flow.
6//!
7//! # State Transitions
8//!
9//! ```text
10//!          Let x be value
11//!               │
12//!               ▼
13//!           [Owned]
14//!          /        \
15//!    Give x       Show x
16//!        │            │
17//!        ▼            ▼
18//!    [Moved]     [Borrowed]
19//!        │            │
20//!    use x?      use x? ✓
21//!        │
22//!     ERROR: use-after-move
23//! ```
24//!
25//! # Control Flow Awareness
26//!
27//! The checker handles branches by merging states:
28//! - `Moved` in one branch + `Owned` in other = `MaybeMoved`
29//! - Using a `MaybeMoved` variable produces an error
30//!
31//! # Example
32//!
33//! ```text
34//! Let x be 5.
35//! Give x to y.
36//! Show x to show.  ← Error: Cannot use 'x' after giving it away
37//! ```
38
39use std::collections::HashMap;
40use crate::ast::stmt::{Stmt, Expr};
41use crate::intern::{Interner, Symbol};
42use crate::token::Span;
43
44/// Ownership state for a variable
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum VarState {
47    /// Variable is owned and can be used
48    Owned,
49    /// Variable has been moved (Give)
50    Moved,
51    /// Variable might be moved (conditional branch)
52    MaybeMoved,
53    /// Variable is borrowed (Show) - still usable
54    Borrowed,
55}
56
57/// Error type for ownership violations
58#[derive(Debug, Clone)]
59pub struct OwnershipError {
60    pub kind: OwnershipErrorKind,
61    pub span: Span,
62}
63
64#[derive(Debug, Clone)]
65pub enum OwnershipErrorKind {
66    /// Use after move
67    UseAfterMove { variable: String },
68    /// Use after potential move (in conditional)
69    UseAfterMaybeMove { variable: String, branch: String },
70    /// Double move
71    DoubleMoved { variable: String },
72}
73
74impl std::fmt::Display for OwnershipError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match &self.kind {
77            OwnershipErrorKind::UseAfterMove { variable } => {
78                write!(f, "Cannot use '{}' after giving it away.\n\n\
79                    You transferred ownership of '{}' with Give.\n\
80                    Once given, you cannot use it anymore.\n\n\
81                    Tip: Use Show instead to lend without giving up ownership.",
82                    variable, variable)
83            }
84            OwnershipErrorKind::UseAfterMaybeMove { variable, branch } => {
85                write!(f, "Cannot use '{}' - it might have been given away in {}.\n\n\
86                    If the {} branch executes, '{}' will be moved.\n\
87                    Using it afterward is not safe.\n\n\
88                    Tip: Move the usage inside the branch, or restructure to ensure ownership.",
89                    variable, branch, branch, variable)
90            }
91            OwnershipErrorKind::DoubleMoved { variable } => {
92                write!(f, "Cannot give '{}' twice.\n\n\
93                    You already transferred ownership of '{}' with Give.\n\
94                    You cannot give it again.\n\n\
95                    Tip: Consider using Copy to duplicate the value.",
96                    variable, variable)
97            }
98        }
99    }
100}
101
102impl std::error::Error for OwnershipError {}
103
104/// Ownership checker - tracks variable states through control flow
105pub struct OwnershipChecker<'a> {
106    /// Maps variable symbols to their current ownership state
107    state: HashMap<Symbol, VarState>,
108    /// String interner for resolving symbols
109    interner: &'a Interner,
110}
111
112impl<'a> OwnershipChecker<'a> {
113    pub fn new(interner: &'a Interner) -> Self {
114        Self {
115            state: HashMap::new(),
116            interner,
117        }
118    }
119
120    /// Check a program for ownership violations
121    pub fn check_program(&mut self, stmts: &[Stmt<'_>]) -> Result<(), OwnershipError> {
122        self.check_block(stmts)
123    }
124
125    fn check_block(&mut self, stmts: &[Stmt<'_>]) -> Result<(), OwnershipError> {
126        for stmt in stmts {
127            self.check_stmt(stmt)?;
128        }
129        Ok(())
130    }
131
132    fn check_stmt(&mut self, stmt: &Stmt<'_>) -> Result<(), OwnershipError> {
133        match stmt {
134            Stmt::Let { var, value, .. } => {
135                // Check the value expression first
136                self.check_not_moved(value)?;
137                // Register variable as Owned
138                self.state.insert(*var, VarState::Owned);
139            }
140
141            Stmt::Give { object, .. } => {
142                // Check if object is already moved
143                if let Expr::Identifier(sym) = object {
144                    let current = self.state.get(sym).copied().unwrap_or(VarState::Owned);
145                    match current {
146                        VarState::Moved => {
147                            return Err(OwnershipError {
148                                kind: OwnershipErrorKind::DoubleMoved {
149                                    variable: self.interner.resolve(*sym).to_string(),
150                                },
151                                span: Span::default(),
152                            });
153                        }
154                        VarState::MaybeMoved => {
155                            return Err(OwnershipError {
156                                kind: OwnershipErrorKind::UseAfterMaybeMove {
157                                    variable: self.interner.resolve(*sym).to_string(),
158                                    branch: "a previous branch".to_string(),
159                                },
160                                span: Span::default(),
161                            });
162                        }
163                        _ => {
164                            self.state.insert(*sym, VarState::Moved);
165                        }
166                    }
167                } else {
168                    // For complex expressions, just check they're not moved
169                    self.check_not_moved(object)?;
170                }
171            }
172
173            Stmt::Show { object, .. } => {
174                // Check if object is moved before borrowing
175                self.check_not_moved(object)?;
176                // Mark as borrowed (still usable)
177                if let Expr::Identifier(sym) = object {
178                    let current = self.state.get(sym).copied();
179                    if current == Some(VarState::Owned) || current.is_none() {
180                        self.state.insert(*sym, VarState::Borrowed);
181                    }
182                }
183            }
184
185            Stmt::If { then_block, else_block, .. } => {
186                // Clone state before branching
187                let state_before = self.state.clone();
188
189                // Check then branch
190                self.check_block(then_block)?;
191                let state_after_then = self.state.clone();
192
193                // Check else branch (if exists)
194                let state_after_else = if let Some(else_b) = else_block {
195                    self.state = state_before.clone();
196                    self.check_block(else_b)?;
197                    self.state.clone()
198                } else {
199                    state_before.clone()
200                };
201
202                // Merge states: MaybeMoved if moved in any branch
203                self.state = self.merge_states(&state_after_then, &state_after_else);
204            }
205
206            Stmt::While { body, .. } => {
207                // Clone state before loop
208                let state_before = self.state.clone();
209
210                // Check body once
211                self.check_block(body)?;
212                let state_after_body = self.state.clone();
213
214                // Merge: if moved in body, mark as MaybeMoved
215                // (loop might not execute, or might execute multiple times)
216                self.state = self.merge_states(&state_before, &state_after_body);
217            }
218
219            Stmt::Repeat { body, .. } => {
220                // Check body once
221                self.check_block(body)?;
222            }
223
224            Stmt::Zone { body, .. } => {
225                self.check_block(body)?;
226            }
227
228            Stmt::Inspect { arms, .. } => {
229                if arms.is_empty() {
230                    return Ok(());
231                }
232
233                // Clone state before branches
234                let state_before = self.state.clone();
235                let mut branch_states = Vec::new();
236
237                for arm in arms {
238                    self.state = state_before.clone();
239                    self.check_block(arm.body)?;
240                    branch_states.push(self.state.clone());
241                }
242
243                // Merge all branch states
244                if let Some(first) = branch_states.first() {
245                    let mut merged = first.clone();
246                    for state in branch_states.iter().skip(1) {
247                        merged = self.merge_states(&merged, state);
248                    }
249                    self.state = merged;
250                }
251            }
252
253            Stmt::Return { value: Some(expr) } => {
254                self.check_not_moved(expr)?;
255            }
256
257            Stmt::Return { value: None } => {}
258
259            Stmt::Set { value, .. } => {
260                self.check_not_moved(value)?;
261            }
262
263            Stmt::Call { args, .. } => {
264                for arg in args {
265                    self.check_not_moved(arg)?;
266                }
267            }
268
269            // Escape blocks are opaque to ownership analysis — the Rust compiler
270            // catches use-after-move in the generated code
271            Stmt::Escape { .. } => {}
272
273            // Other statements don't affect ownership
274            _ => {}
275        }
276        Ok(())
277    }
278
279    /// Check that an expression doesn't reference a moved variable
280    fn check_not_moved(&self, expr: &Expr<'_>) -> Result<(), OwnershipError> {
281        match expr {
282            Expr::Identifier(sym) => {
283                match self.state.get(sym).copied() {
284                    Some(VarState::Moved) => {
285                        Err(OwnershipError {
286                            kind: OwnershipErrorKind::UseAfterMove {
287                                variable: self.interner.resolve(*sym).to_string(),
288                            },
289                            span: Span::default(),
290                        })
291                    }
292                    Some(VarState::MaybeMoved) => {
293                        Err(OwnershipError {
294                            kind: OwnershipErrorKind::UseAfterMaybeMove {
295                                variable: self.interner.resolve(*sym).to_string(),
296                                branch: "a conditional branch".to_string(),
297                            },
298                            span: Span::default(),
299                        })
300                    }
301                    _ => Ok(())
302                }
303            }
304            Expr::BinaryOp { left, right, .. } => {
305                self.check_not_moved(left)?;
306                self.check_not_moved(right)?;
307                Ok(())
308            }
309            Expr::FieldAccess { object, .. } => {
310                self.check_not_moved(object)
311            }
312            Expr::Index { collection, index } => {
313                self.check_not_moved(collection)?;
314                self.check_not_moved(index)?;
315                Ok(())
316            }
317            Expr::Slice { collection, start, end } => {
318                self.check_not_moved(collection)?;
319                self.check_not_moved(start)?;
320                self.check_not_moved(end)?;
321                Ok(())
322            }
323            Expr::Call { args, .. } => {
324                for arg in args {
325                    self.check_not_moved(arg)?;
326                }
327                Ok(())
328            }
329            Expr::List(items) | Expr::Tuple(items) => {
330                for item in items {
331                    self.check_not_moved(item)?;
332                }
333                Ok(())
334            }
335            Expr::Range { start, end } => {
336                self.check_not_moved(start)?;
337                self.check_not_moved(end)?;
338                Ok(())
339            }
340            Expr::New { init_fields, .. } => {
341                for (_, field_expr) in init_fields {
342                    self.check_not_moved(field_expr)?;
343                }
344                Ok(())
345            }
346            Expr::NewVariant { fields, .. } => {
347                for (_, field_expr) in fields {
348                    self.check_not_moved(field_expr)?;
349                }
350                Ok(())
351            }
352            Expr::Copy { expr } | Expr::Give { value: expr } | Expr::Length { collection: expr } => {
353                self.check_not_moved(expr)
354            }
355            Expr::ManifestOf { zone } => {
356                self.check_not_moved(zone)
357            }
358            Expr::ChunkAt { index, zone } => {
359                self.check_not_moved(index)?;
360                self.check_not_moved(zone)
361            }
362            Expr::Contains { collection, value } => {
363                self.check_not_moved(collection)?;
364                self.check_not_moved(value)
365            }
366            Expr::Union { left, right } | Expr::Intersection { left, right } => {
367                self.check_not_moved(left)?;
368                self.check_not_moved(right)
369            }
370            // Escape expressions are opaque — the Rust compiler handles ownership for raw code
371            Expr::Escape { .. } => Ok(()),
372
373            // Literals are always safe
374            Expr::Literal(_) => Ok(()),
375        }
376    }
377
378    /// Merge two branch states - if moved in either, mark as MaybeMoved
379    fn merge_states(
380        &self,
381        state_a: &HashMap<Symbol, VarState>,
382        state_b: &HashMap<Symbol, VarState>,
383    ) -> HashMap<Symbol, VarState> {
384        let mut merged = state_a.clone();
385
386        // Merge keys from state_b
387        for (sym, state_b_val) in state_b {
388            let state_a_val = state_a.get(sym).copied().unwrap_or(VarState::Owned);
389
390            let merged_val = match (state_a_val, *state_b_val) {
391                // Both moved = definitely moved
392                (VarState::Moved, VarState::Moved) => VarState::Moved,
393                // One moved, one not = maybe moved
394                (VarState::Moved, _) | (_, VarState::Moved) => VarState::MaybeMoved,
395                // Any maybe moved = maybe moved
396                (VarState::MaybeMoved, _) | (_, VarState::MaybeMoved) => VarState::MaybeMoved,
397                // Both borrowed = borrowed
398                (VarState::Borrowed, VarState::Borrowed) => VarState::Borrowed,
399                // Borrowed + Owned = Borrowed (conservative)
400                (VarState::Borrowed, _) | (_, VarState::Borrowed) => VarState::Borrowed,
401                // Both owned = owned
402                (VarState::Owned, VarState::Owned) => VarState::Owned,
403            };
404
405            merged.insert(*sym, merged_val);
406        }
407
408        // Also check keys only in state_a
409        for sym in state_a.keys() {
410            if !state_b.contains_key(sym) {
411                // Variable exists in one branch but not other - keep state_a value
412                // (already in merged)
413            }
414        }
415
416        merged
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_ownership_checker_basic() {
426        let interner = Interner::new();
427        let checker = OwnershipChecker::new(&interner);
428        assert!(checker.state.is_empty());
429    }
430}