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            // Other statements don't affect ownership
270            _ => {}
271        }
272        Ok(())
273    }
274
275    /// Check that an expression doesn't reference a moved variable
276    fn check_not_moved(&self, expr: &Expr<'_>) -> Result<(), OwnershipError> {
277        match expr {
278            Expr::Identifier(sym) => {
279                match self.state.get(sym).copied() {
280                    Some(VarState::Moved) => {
281                        Err(OwnershipError {
282                            kind: OwnershipErrorKind::UseAfterMove {
283                                variable: self.interner.resolve(*sym).to_string(),
284                            },
285                            span: Span::default(),
286                        })
287                    }
288                    Some(VarState::MaybeMoved) => {
289                        Err(OwnershipError {
290                            kind: OwnershipErrorKind::UseAfterMaybeMove {
291                                variable: self.interner.resolve(*sym).to_string(),
292                                branch: "a conditional branch".to_string(),
293                            },
294                            span: Span::default(),
295                        })
296                    }
297                    _ => Ok(())
298                }
299            }
300            Expr::BinaryOp { left, right, .. } => {
301                self.check_not_moved(left)?;
302                self.check_not_moved(right)?;
303                Ok(())
304            }
305            Expr::FieldAccess { object, .. } => {
306                self.check_not_moved(object)
307            }
308            Expr::Index { collection, index } => {
309                self.check_not_moved(collection)?;
310                self.check_not_moved(index)?;
311                Ok(())
312            }
313            Expr::Slice { collection, start, end } => {
314                self.check_not_moved(collection)?;
315                self.check_not_moved(start)?;
316                self.check_not_moved(end)?;
317                Ok(())
318            }
319            Expr::Call { args, .. } => {
320                for arg in args {
321                    self.check_not_moved(arg)?;
322                }
323                Ok(())
324            }
325            Expr::List(items) | Expr::Tuple(items) => {
326                for item in items {
327                    self.check_not_moved(item)?;
328                }
329                Ok(())
330            }
331            Expr::Range { start, end } => {
332                self.check_not_moved(start)?;
333                self.check_not_moved(end)?;
334                Ok(())
335            }
336            Expr::New { init_fields, .. } => {
337                for (_, field_expr) in init_fields {
338                    self.check_not_moved(field_expr)?;
339                }
340                Ok(())
341            }
342            Expr::NewVariant { fields, .. } => {
343                for (_, field_expr) in fields {
344                    self.check_not_moved(field_expr)?;
345                }
346                Ok(())
347            }
348            Expr::Copy { expr } | Expr::Length { collection: expr } => {
349                self.check_not_moved(expr)
350            }
351            Expr::ManifestOf { zone } => {
352                self.check_not_moved(zone)
353            }
354            Expr::ChunkAt { index, zone } => {
355                self.check_not_moved(index)?;
356                self.check_not_moved(zone)
357            }
358            Expr::Contains { collection, value } => {
359                self.check_not_moved(collection)?;
360                self.check_not_moved(value)
361            }
362            Expr::Union { left, right } | Expr::Intersection { left, right } => {
363                self.check_not_moved(left)?;
364                self.check_not_moved(right)
365            }
366            // Literals are always safe
367            Expr::Literal(_) => Ok(()),
368        }
369    }
370
371    /// Merge two branch states - if moved in either, mark as MaybeMoved
372    fn merge_states(
373        &self,
374        state_a: &HashMap<Symbol, VarState>,
375        state_b: &HashMap<Symbol, VarState>,
376    ) -> HashMap<Symbol, VarState> {
377        let mut merged = state_a.clone();
378
379        // Merge keys from state_b
380        for (sym, state_b_val) in state_b {
381            let state_a_val = state_a.get(sym).copied().unwrap_or(VarState::Owned);
382
383            let merged_val = match (state_a_val, *state_b_val) {
384                // Both moved = definitely moved
385                (VarState::Moved, VarState::Moved) => VarState::Moved,
386                // One moved, one not = maybe moved
387                (VarState::Moved, _) | (_, VarState::Moved) => VarState::MaybeMoved,
388                // Any maybe moved = maybe moved
389                (VarState::MaybeMoved, _) | (_, VarState::MaybeMoved) => VarState::MaybeMoved,
390                // Both borrowed = borrowed
391                (VarState::Borrowed, VarState::Borrowed) => VarState::Borrowed,
392                // Borrowed + Owned = Borrowed (conservative)
393                (VarState::Borrowed, _) | (_, VarState::Borrowed) => VarState::Borrowed,
394                // Both owned = owned
395                (VarState::Owned, VarState::Owned) => VarState::Owned,
396            };
397
398            merged.insert(*sym, merged_val);
399        }
400
401        // Also check keys only in state_a
402        for sym in state_a.keys() {
403            if !state_b.contains_key(sym) {
404                // Variable exists in one branch but not other - keep state_a value
405                // (already in merged)
406            }
407        }
408
409        merged
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn test_ownership_checker_basic() {
419        let interner = Interner::new();
420        let checker = OwnershipChecker::new(&interner);
421        assert!(checker.state.is_empty());
422    }
423}