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::{BinaryOpKind, Literal, Stmt, Expr, TypeExpr};
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    /// Tracks whether each variable is a Copy type (true = Copy, absent = unknown/Copy)
109    types: HashMap<Symbol, bool>,
110    /// String interner for resolving symbols
111    interner: &'a Interner,
112}
113
114impl<'a> OwnershipChecker<'a> {
115    pub fn new(interner: &'a Interner) -> Self {
116        Self {
117            state: HashMap::new(),
118            types: HashMap::new(),
119            interner,
120        }
121    }
122
123    /// Access the current variable ownership states.
124    pub fn var_states(&self) -> &HashMap<Symbol, VarState> {
125        &self.state
126    }
127
128    /// Returns true if a symbol is known to be a Copy type.
129    /// Unknown types conservatively return true (won't produce false positives).
130    fn is_copy_sym(&self, sym: Symbol) -> bool {
131        self.types.get(&sym).copied().unwrap_or(true)
132    }
133
134    /// Infer whether an expression produces a Copy type.
135    /// Conservative: returns true (Copy) when uncertain.
136    fn infer_copy_from_expr(&self, expr: &Expr) -> bool {
137        match expr {
138            Expr::Literal(Literal::Number(_)) => true,
139            Expr::Literal(Literal::Float(_)) => true,
140            Expr::Literal(Literal::Boolean(_)) => true,
141            Expr::Literal(Literal::Nothing) => true,
142            Expr::Literal(Literal::Text(_)) => false,
143            Expr::Identifier(sym) => self.is_copy_sym(*sym),
144            Expr::New { .. } => false,
145            Expr::List(_) => false,
146            Expr::InterpolatedString(_) => false,
147            Expr::Copy { .. } => true,
148            Expr::BinaryOp { op: BinaryOpKind::Concat, .. } => false,
149            Expr::BinaryOp { .. } => true,
150            Expr::Contains { .. } => true,
151            Expr::Length { .. } => true,
152            _ => true,
153        }
154    }
155
156    /// After an expression has been validated, walk it to mark non-Copy
157    /// function call arguments as Moved.
158    fn mark_moves_in_expr(&mut self, expr: &Expr) {
159        match expr {
160            Expr::Call { args, .. } | Expr::CallExpr { args, .. } => {
161                for arg in args.iter() {
162                    if let Expr::Identifier(sym) = arg {
163                        if !self.is_copy_sym(*sym) {
164                            self.state.insert(*sym, VarState::Moved);
165                        }
166                    }
167                    self.mark_moves_in_expr(arg);
168                }
169            }
170            Expr::BinaryOp { left, right, .. } => {
171                self.mark_moves_in_expr(left);
172                self.mark_moves_in_expr(right);
173            }
174            Expr::Index { collection, index } => {
175                self.mark_moves_in_expr(collection);
176                self.mark_moves_in_expr(index);
177            }
178            Expr::FieldAccess { object, .. } => {
179                self.mark_moves_in_expr(object);
180            }
181            _ => {}
182        }
183    }
184
185    /// Infer Copy-ness from a TypeExpr (function parameter type annotation).
186    /// Conservative: returns true (Copy) when uncertain.
187    fn infer_copy_from_type_name(&self, ty: &TypeExpr) -> bool {
188        match ty {
189            TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
190                let name = self.interner.resolve(*sym);
191                matches!(name, "Int" | "Nat" | "Float" | "Bool" | "Char" | "Byte")
192            }
193            TypeExpr::Generic { .. } => false,
194            TypeExpr::Function { .. } => true,
195            _ => true,
196        }
197    }
198
199    /// Check a program for ownership violations
200    pub fn check_program(&mut self, stmts: &[Stmt<'_>]) -> Result<(), OwnershipError> {
201        self.check_block(stmts)
202    }
203
204    fn check_block(&mut self, stmts: &[Stmt<'_>]) -> Result<(), OwnershipError> {
205        for stmt in stmts {
206            self.check_stmt(stmt)?;
207        }
208        Ok(())
209    }
210
211    fn check_stmt(&mut self, stmt: &Stmt<'_>) -> Result<(), OwnershipError> {
212        match stmt {
213            Stmt::Let { var, value, .. } => {
214                // Check the value expression first
215                self.check_not_moved(value)?;
216                // Mark non-Copy identifiers used as values as Moved
217                if let Expr::Identifier(sym) = value {
218                    if !self.is_copy_sym(*sym) {
219                        self.state.insert(*sym, VarState::Moved);
220                    }
221                }
222                // Mark non-Copy function call arguments as Moved
223                self.mark_moves_in_expr(value);
224                // Register variable as Owned and track its type
225                let is_copy = self.infer_copy_from_expr(value);
226                self.state.insert(*var, VarState::Owned);
227                self.types.insert(*var, is_copy);
228            }
229
230            Stmt::Give { object, .. } => {
231                // Check if object is already moved
232                if let Expr::Identifier(sym) = object {
233                    let current = self.state.get(sym).copied().unwrap_or(VarState::Owned);
234                    match current {
235                        VarState::Moved => {
236                            return Err(OwnershipError {
237                                kind: OwnershipErrorKind::DoubleMoved {
238                                    variable: self.interner.resolve(*sym).to_string(),
239                                },
240                                span: Span::default(),
241                            });
242                        }
243                        VarState::MaybeMoved => {
244                            return Err(OwnershipError {
245                                kind: OwnershipErrorKind::UseAfterMaybeMove {
246                                    variable: self.interner.resolve(*sym).to_string(),
247                                    branch: "a previous branch".to_string(),
248                                },
249                                span: Span::default(),
250                            });
251                        }
252                        _ => {
253                            self.state.insert(*sym, VarState::Moved);
254                        }
255                    }
256                } else {
257                    // For complex expressions, just check they're not moved
258                    self.check_not_moved(object)?;
259                }
260            }
261
262            Stmt::Show { object, .. } => {
263                // Check if object is moved before borrowing
264                self.check_not_moved(object)?;
265                // Mark as borrowed (still usable)
266                if let Expr::Identifier(sym) = object {
267                    let current = self.state.get(sym).copied();
268                    if current == Some(VarState::Owned) || current.is_none() {
269                        self.state.insert(*sym, VarState::Borrowed);
270                    }
271                }
272            }
273
274            Stmt::If { then_block, else_block, .. } => {
275                // Clone state before branching
276                let state_before = self.state.clone();
277
278                // Check then branch
279                self.check_block(then_block)?;
280                let state_after_then = self.state.clone();
281
282                // Check else branch (if exists)
283                let state_after_else = if let Some(else_b) = else_block {
284                    self.state = state_before.clone();
285                    self.check_block(else_b)?;
286                    self.state.clone()
287                } else {
288                    state_before.clone()
289                };
290
291                // Merge states: MaybeMoved if moved in any branch
292                self.state = self.merge_states(&state_after_then, &state_after_else);
293            }
294
295            Stmt::While { body, .. } => {
296                // Clone state before loop
297                let state_before = self.state.clone();
298
299                // Check body once
300                self.check_block(body)?;
301                let state_after_body = self.state.clone();
302
303                // Merge: if moved in body, mark as MaybeMoved
304                // (loop might not execute, or might execute multiple times)
305                self.state = self.merge_states(&state_before, &state_after_body);
306            }
307
308            Stmt::Repeat { body, .. } => {
309                // Check body once
310                self.check_block(body)?;
311            }
312
313            Stmt::Zone { body, .. } => {
314                self.check_block(body)?;
315            }
316
317            Stmt::Inspect { arms, .. } => {
318                if arms.is_empty() {
319                    return Ok(());
320                }
321
322                // Clone state before branches
323                let state_before = self.state.clone();
324                let mut branch_states = Vec::new();
325
326                for arm in arms {
327                    self.state = state_before.clone();
328                    self.check_block(arm.body)?;
329                    branch_states.push(self.state.clone());
330                }
331
332                // Merge all branch states
333                if let Some(first) = branch_states.first() {
334                    let mut merged = first.clone();
335                    for state in branch_states.iter().skip(1) {
336                        merged = self.merge_states(&merged, state);
337                    }
338                    self.state = merged;
339                }
340            }
341
342            Stmt::Return { value: Some(expr) } => {
343                self.check_not_moved(expr)?;
344                self.mark_moves_in_expr(expr);
345            }
346
347            Stmt::Return { value: None } => {}
348
349            Stmt::Set { value, .. } => {
350                self.check_not_moved(value)?;
351                // Mark non-Copy function call arguments as Moved
352                self.mark_moves_in_expr(value);
353            }
354
355            Stmt::Call { args, .. } => {
356                for arg in args.iter() {
357                    self.check_not_moved(arg)?;
358                }
359                // Mark non-Copy identifier arguments as Moved
360                for arg in args.iter() {
361                    if let Expr::Identifier(sym) = arg {
362                        if !self.is_copy_sym(*sym) {
363                            self.state.insert(*sym, VarState::Moved);
364                        }
365                    }
366                }
367            }
368
369            Stmt::FunctionDef { params, body, .. } => {
370                // Save state — function body is a separate scope
371                let saved_state = self.state.clone();
372                let saved_types = self.types.clone();
373                // Register parameters as Owned with inferred Copy-ness
374                for (param_sym, param_type) in params.iter() {
375                    self.state.insert(*param_sym, VarState::Owned);
376                    let is_copy = self.infer_copy_from_type_name(param_type);
377                    self.types.insert(*param_sym, is_copy);
378                }
379                self.check_block(body)?;
380                self.state = saved_state;
381                self.types = saved_types;
382            }
383
384            // Escape blocks are opaque to ownership analysis — the Rust compiler
385            // catches use-after-move in the generated code
386            Stmt::Escape { .. } => {}
387
388            // Other statements don't affect ownership
389            _ => {}
390        }
391        Ok(())
392    }
393
394    /// Check that an expression doesn't reference a moved variable
395    fn check_not_moved(&self, expr: &Expr<'_>) -> Result<(), OwnershipError> {
396        match expr {
397            Expr::InterpolatedString(parts) => {
398                for part in parts {
399                    if let crate::ast::stmt::StringPart::Expr { value, .. } = part {
400                        self.check_not_moved(value)?;
401                    }
402                }
403                Ok(())
404            }
405            Expr::Identifier(sym) => {
406                match self.state.get(sym).copied() {
407                    Some(VarState::Moved) => {
408                        Err(OwnershipError {
409                            kind: OwnershipErrorKind::UseAfterMove {
410                                variable: self.interner.resolve(*sym).to_string(),
411                            },
412                            span: Span::default(),
413                        })
414                    }
415                    Some(VarState::MaybeMoved) => {
416                        Err(OwnershipError {
417                            kind: OwnershipErrorKind::UseAfterMaybeMove {
418                                variable: self.interner.resolve(*sym).to_string(),
419                                branch: "a conditional branch".to_string(),
420                            },
421                            span: Span::default(),
422                        })
423                    }
424                    _ => Ok(())
425                }
426            }
427            Expr::BinaryOp { left, right, .. } => {
428                self.check_not_moved(left)?;
429                self.check_not_moved(right)?;
430                Ok(())
431            }
432            Expr::FieldAccess { object, .. } => {
433                self.check_not_moved(object)
434            }
435            Expr::Index { collection, index } => {
436                self.check_not_moved(collection)?;
437                self.check_not_moved(index)?;
438                Ok(())
439            }
440            Expr::Slice { collection, start, end } => {
441                self.check_not_moved(collection)?;
442                self.check_not_moved(start)?;
443                self.check_not_moved(end)?;
444                Ok(())
445            }
446            Expr::Call { args, .. } => {
447                for arg in args {
448                    self.check_not_moved(arg)?;
449                }
450                Ok(())
451            }
452            Expr::List(items) | Expr::Tuple(items) => {
453                for item in items {
454                    self.check_not_moved(item)?;
455                }
456                Ok(())
457            }
458            Expr::Range { start, end } => {
459                self.check_not_moved(start)?;
460                self.check_not_moved(end)?;
461                Ok(())
462            }
463            Expr::New { init_fields, .. } => {
464                for (_, field_expr) in init_fields {
465                    self.check_not_moved(field_expr)?;
466                }
467                Ok(())
468            }
469            Expr::NewVariant { fields, .. } => {
470                for (_, field_expr) in fields {
471                    self.check_not_moved(field_expr)?;
472                }
473                Ok(())
474            }
475            Expr::Copy { expr } | Expr::Give { value: expr } | Expr::Length { collection: expr }
476            | Expr::Not { operand: expr } => {
477                self.check_not_moved(expr)
478            }
479            Expr::ManifestOf { zone } => {
480                self.check_not_moved(zone)
481            }
482            Expr::ChunkAt { index, zone } => {
483                self.check_not_moved(index)?;
484                self.check_not_moved(zone)
485            }
486            Expr::Contains { collection, value } => {
487                self.check_not_moved(collection)?;
488                self.check_not_moved(value)
489            }
490            Expr::Union { left, right } | Expr::Intersection { left, right } => {
491                self.check_not_moved(left)?;
492                self.check_not_moved(right)
493            }
494            Expr::WithCapacity { value, capacity } => {
495                self.check_not_moved(value)?;
496                self.check_not_moved(capacity)
497            }
498            Expr::OptionSome { value } => self.check_not_moved(value),
499            Expr::OptionNone => Ok(()),
500
501            // Escape expressions are opaque — the Rust compiler handles ownership for raw code
502            Expr::Escape { .. } => Ok(()),
503
504            // Closures capture by cloning — no ownership transfer at creation time.
505            // We only check the expression body for moved variables; block bodies
506            // create their own scope so ownership is handled there.
507            Expr::Closure { body, .. } => {
508                match body {
509                    crate::ast::stmt::ClosureBody::Expression(expr) => {
510                        self.check_not_moved(expr)
511                    }
512                    crate::ast::stmt::ClosureBody::Block(_) => Ok(()),
513                }
514            }
515
516            Expr::CallExpr { callee, args } => {
517                self.check_not_moved(callee)?;
518                for arg in args {
519                    self.check_not_moved(arg)?;
520                }
521                Ok(())
522            }
523
524            // Literals are always safe
525            Expr::Literal(_) => Ok(()),
526        }
527    }
528
529    /// Merge two branch states - if moved in either, mark as MaybeMoved
530    fn merge_states(
531        &self,
532        state_a: &HashMap<Symbol, VarState>,
533        state_b: &HashMap<Symbol, VarState>,
534    ) -> HashMap<Symbol, VarState> {
535        let mut merged = state_a.clone();
536
537        // Merge keys from state_b
538        for (sym, state_b_val) in state_b {
539            let state_a_val = state_a.get(sym).copied().unwrap_or(VarState::Owned);
540
541            let merged_val = match (state_a_val, *state_b_val) {
542                // Both moved = definitely moved
543                (VarState::Moved, VarState::Moved) => VarState::Moved,
544                // One moved, one not = maybe moved
545                (VarState::Moved, _) | (_, VarState::Moved) => VarState::MaybeMoved,
546                // Any maybe moved = maybe moved
547                (VarState::MaybeMoved, _) | (_, VarState::MaybeMoved) => VarState::MaybeMoved,
548                // Both borrowed = borrowed
549                (VarState::Borrowed, VarState::Borrowed) => VarState::Borrowed,
550                // Borrowed + Owned = Borrowed (conservative)
551                (VarState::Borrowed, _) | (_, VarState::Borrowed) => VarState::Borrowed,
552                // Both owned = owned
553                (VarState::Owned, VarState::Owned) => VarState::Owned,
554            };
555
556            merged.insert(*sym, merged_val);
557        }
558
559        // Also check keys only in state_a
560        for sym in state_a.keys() {
561            if !state_b.contains_key(sym) {
562                // Variable exists in one branch but not other - keep state_a value
563                // (already in merged)
564            }
565        }
566
567        merged
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    #[test]
576    fn test_ownership_checker_basic() {
577        let interner = Interner::new();
578        let checker = OwnershipChecker::new(&interner);
579        assert!(checker.state.is_empty());
580    }
581}