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