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    /// Access the current variable ownership states.
121    pub fn var_states(&self) -> &HashMap<Symbol, VarState> {
122        &self.state
123    }
124
125    /// Check a program for ownership violations
126    pub fn check_program(&mut self, stmts: &[Stmt<'_>]) -> Result<(), OwnershipError> {
127        self.check_block(stmts)
128    }
129
130    fn check_block(&mut self, stmts: &[Stmt<'_>]) -> Result<(), OwnershipError> {
131        for stmt in stmts {
132            self.check_stmt(stmt)?;
133        }
134        Ok(())
135    }
136
137    fn check_stmt(&mut self, stmt: &Stmt<'_>) -> Result<(), OwnershipError> {
138        match stmt {
139            Stmt::Let { var, value, .. } => {
140                // Check the value expression first
141                self.check_not_moved(value)?;
142                // Register variable as Owned
143                self.state.insert(*var, VarState::Owned);
144            }
145
146            Stmt::Give { object, .. } => {
147                // Check if object is already moved
148                if let Expr::Identifier(sym) = object {
149                    let current = self.state.get(sym).copied().unwrap_or(VarState::Owned);
150                    match current {
151                        VarState::Moved => {
152                            return Err(OwnershipError {
153                                kind: OwnershipErrorKind::DoubleMoved {
154                                    variable: self.interner.resolve(*sym).to_string(),
155                                },
156                                span: Span::default(),
157                            });
158                        }
159                        VarState::MaybeMoved => {
160                            return Err(OwnershipError {
161                                kind: OwnershipErrorKind::UseAfterMaybeMove {
162                                    variable: self.interner.resolve(*sym).to_string(),
163                                    branch: "a previous branch".to_string(),
164                                },
165                                span: Span::default(),
166                            });
167                        }
168                        _ => {
169                            self.state.insert(*sym, VarState::Moved);
170                        }
171                    }
172                } else {
173                    // For complex expressions, just check they're not moved
174                    self.check_not_moved(object)?;
175                }
176            }
177
178            Stmt::Show { object, .. } => {
179                // Check if object is moved before borrowing
180                self.check_not_moved(object)?;
181                // Mark as borrowed (still usable)
182                if let Expr::Identifier(sym) = object {
183                    let current = self.state.get(sym).copied();
184                    if current == Some(VarState::Owned) || current.is_none() {
185                        self.state.insert(*sym, VarState::Borrowed);
186                    }
187                }
188            }
189
190            Stmt::If { then_block, else_block, .. } => {
191                // Clone state before branching
192                let state_before = self.state.clone();
193
194                // Check then branch
195                self.check_block(then_block)?;
196                let state_after_then = self.state.clone();
197
198                // Check else branch (if exists)
199                let state_after_else = if let Some(else_b) = else_block {
200                    self.state = state_before.clone();
201                    self.check_block(else_b)?;
202                    self.state.clone()
203                } else {
204                    state_before.clone()
205                };
206
207                // Merge states: MaybeMoved if moved in any branch
208                self.state = self.merge_states(&state_after_then, &state_after_else);
209            }
210
211            Stmt::While { body, .. } => {
212                // Clone state before loop
213                let state_before = self.state.clone();
214
215                // Check body once
216                self.check_block(body)?;
217                let state_after_body = self.state.clone();
218
219                // Merge: if moved in body, mark as MaybeMoved
220                // (loop might not execute, or might execute multiple times)
221                self.state = self.merge_states(&state_before, &state_after_body);
222            }
223
224            Stmt::Repeat { body, .. } => {
225                // Check body once
226                self.check_block(body)?;
227            }
228
229            Stmt::Zone { body, .. } => {
230                self.check_block(body)?;
231            }
232
233            Stmt::Inspect { arms, .. } => {
234                if arms.is_empty() {
235                    return Ok(());
236                }
237
238                // Clone state before branches
239                let state_before = self.state.clone();
240                let mut branch_states = Vec::new();
241
242                for arm in arms {
243                    self.state = state_before.clone();
244                    self.check_block(arm.body)?;
245                    branch_states.push(self.state.clone());
246                }
247
248                // Merge all branch states
249                if let Some(first) = branch_states.first() {
250                    let mut merged = first.clone();
251                    for state in branch_states.iter().skip(1) {
252                        merged = self.merge_states(&merged, state);
253                    }
254                    self.state = merged;
255                }
256            }
257
258            Stmt::Return { value: Some(expr) } => {
259                self.check_not_moved(expr)?;
260            }
261
262            Stmt::Return { value: None } => {}
263
264            Stmt::Set { value, .. } => {
265                self.check_not_moved(value)?;
266            }
267
268            Stmt::Call { args, .. } => {
269                for arg in args {
270                    self.check_not_moved(arg)?;
271                }
272            }
273
274            // Escape blocks are opaque to ownership analysis — the Rust compiler
275            // catches use-after-move in the generated code
276            Stmt::Escape { .. } => {}
277
278            // Other statements don't affect ownership
279            _ => {}
280        }
281        Ok(())
282    }
283
284    /// Check that an expression doesn't reference a moved variable
285    fn check_not_moved(&self, expr: &Expr<'_>) -> Result<(), OwnershipError> {
286        match expr {
287            Expr::Identifier(sym) => {
288                match self.state.get(sym).copied() {
289                    Some(VarState::Moved) => {
290                        Err(OwnershipError {
291                            kind: OwnershipErrorKind::UseAfterMove {
292                                variable: self.interner.resolve(*sym).to_string(),
293                            },
294                            span: Span::default(),
295                        })
296                    }
297                    Some(VarState::MaybeMoved) => {
298                        Err(OwnershipError {
299                            kind: OwnershipErrorKind::UseAfterMaybeMove {
300                                variable: self.interner.resolve(*sym).to_string(),
301                                branch: "a conditional branch".to_string(),
302                            },
303                            span: Span::default(),
304                        })
305                    }
306                    _ => Ok(())
307                }
308            }
309            Expr::BinaryOp { left, right, .. } => {
310                self.check_not_moved(left)?;
311                self.check_not_moved(right)?;
312                Ok(())
313            }
314            Expr::FieldAccess { object, .. } => {
315                self.check_not_moved(object)
316            }
317            Expr::Index { collection, index } => {
318                self.check_not_moved(collection)?;
319                self.check_not_moved(index)?;
320                Ok(())
321            }
322            Expr::Slice { collection, start, end } => {
323                self.check_not_moved(collection)?;
324                self.check_not_moved(start)?;
325                self.check_not_moved(end)?;
326                Ok(())
327            }
328            Expr::Call { args, .. } => {
329                for arg in args {
330                    self.check_not_moved(arg)?;
331                }
332                Ok(())
333            }
334            Expr::List(items) | Expr::Tuple(items) => {
335                for item in items {
336                    self.check_not_moved(item)?;
337                }
338                Ok(())
339            }
340            Expr::Range { start, end } => {
341                self.check_not_moved(start)?;
342                self.check_not_moved(end)?;
343                Ok(())
344            }
345            Expr::New { init_fields, .. } => {
346                for (_, field_expr) in init_fields {
347                    self.check_not_moved(field_expr)?;
348                }
349                Ok(())
350            }
351            Expr::NewVariant { fields, .. } => {
352                for (_, field_expr) in fields {
353                    self.check_not_moved(field_expr)?;
354                }
355                Ok(())
356            }
357            Expr::Copy { expr } | Expr::Give { value: expr } | Expr::Length { collection: expr } => {
358                self.check_not_moved(expr)
359            }
360            Expr::ManifestOf { zone } => {
361                self.check_not_moved(zone)
362            }
363            Expr::ChunkAt { index, zone } => {
364                self.check_not_moved(index)?;
365                self.check_not_moved(zone)
366            }
367            Expr::Contains { collection, value } => {
368                self.check_not_moved(collection)?;
369                self.check_not_moved(value)
370            }
371            Expr::Union { left, right } | Expr::Intersection { left, right } => {
372                self.check_not_moved(left)?;
373                self.check_not_moved(right)
374            }
375            Expr::OptionSome { value } => self.check_not_moved(value),
376            Expr::OptionNone => Ok(()),
377
378            // Escape expressions are opaque — the Rust compiler handles ownership for raw code
379            Expr::Escape { .. } => Ok(()),
380
381            // Literals are always safe
382            Expr::Literal(_) => Ok(()),
383        }
384    }
385
386    /// Merge two branch states - if moved in either, mark as MaybeMoved
387    fn merge_states(
388        &self,
389        state_a: &HashMap<Symbol, VarState>,
390        state_b: &HashMap<Symbol, VarState>,
391    ) -> HashMap<Symbol, VarState> {
392        let mut merged = state_a.clone();
393
394        // Merge keys from state_b
395        for (sym, state_b_val) in state_b {
396            let state_a_val = state_a.get(sym).copied().unwrap_or(VarState::Owned);
397
398            let merged_val = match (state_a_val, *state_b_val) {
399                // Both moved = definitely moved
400                (VarState::Moved, VarState::Moved) => VarState::Moved,
401                // One moved, one not = maybe moved
402                (VarState::Moved, _) | (_, VarState::Moved) => VarState::MaybeMoved,
403                // Any maybe moved = maybe moved
404                (VarState::MaybeMoved, _) | (_, VarState::MaybeMoved) => VarState::MaybeMoved,
405                // Both borrowed = borrowed
406                (VarState::Borrowed, VarState::Borrowed) => VarState::Borrowed,
407                // Borrowed + Owned = Borrowed (conservative)
408                (VarState::Borrowed, _) | (_, VarState::Borrowed) => VarState::Borrowed,
409                // Both owned = owned
410                (VarState::Owned, VarState::Owned) => VarState::Owned,
411            };
412
413            merged.insert(*sym, merged_val);
414        }
415
416        // Also check keys only in state_a
417        for sym in state_a.keys() {
418            if !state_b.contains_key(sym) {
419                // Variable exists in one branch but not other - keep state_a value
420                // (already in merged)
421            }
422        }
423
424        merged
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_ownership_checker_basic() {
434        let interner = Interner::new();
435        let checker = OwnershipChecker::new(&interner);
436        assert!(checker.state.is_empty());
437    }
438}