Skip to main content

shape_vm/mir/
analysis.rs

1//! Borrow analysis results — the single source of truth.
2//!
3//! `BorrowAnalysis` is the shared result struct consumed by:
4//! - The compiler (codegen decisions: move vs clone)
5//! - The LSP (inlay hints, borrow windows, hover info)
6//! - The diagnostic engine (error messages, repair suggestions)
7//!
8//! **DRY rule**: Analysis runs ONCE. No consumer re-derives these results.
9
10use super::liveness::LivenessResult;
11use super::types::*;
12use shape_ast::ast::Span;
13use std::collections::HashMap;
14
15/// The complete borrow analysis for a single function.
16/// Produced by the Datafrog solver + liveness analysis.
17/// Consumed (read-only) by compiler, LSP, and diagnostics.
18#[derive(Debug)]
19pub struct BorrowAnalysis {
20    /// Liveness results for move/clone decisions.
21    pub liveness: LivenessResult,
22    /// Active loans at each program point (from Datafrog solver).
23    pub loans_at_point: HashMap<Point, Vec<LoanId>>,
24    /// Loan metadata.
25    pub loans: HashMap<LoanId, LoanInfo>,
26    /// Borrow errors detected by the solver.
27    pub errors: Vec<BorrowError>,
28    /// Move/clone decisions for each assignment of a non-Copy type.
29    pub ownership_decisions: HashMap<Point, OwnershipDecision>,
30    /// Immutability violations (writing to immutable bindings).
31    pub mutability_errors: Vec<MutabilityError>,
32}
33
34/// Information about a single loan (borrow).
35#[derive(Debug, Clone)]
36pub struct LoanInfo {
37    pub id: LoanId,
38    /// The place being borrowed.
39    pub borrowed_place: Place,
40    /// Kind of borrow (shared or exclusive).
41    pub kind: BorrowKind,
42    /// Where the loan was issued.
43    pub issued_at: Point,
44    /// Source span of the borrow expression.
45    pub span: Span,
46}
47
48/// A borrow conflict error with structured data for diagnostics.
49/// The diagnostic engine formats this; consumers never generate error text.
50#[derive(Debug, Clone)]
51pub struct BorrowError {
52    pub kind: BorrowErrorKind,
53    /// Primary span (the conflicting operation).
54    pub span: Span,
55    /// The loan that conflicts.
56    pub conflicting_loan: LoanId,
57    /// Where the conflicting loan was created.
58    pub loan_span: Span,
59    /// Where the loan is still needed (last use).
60    pub last_use_span: Option<Span>,
61    /// Repair candidates, ordered by preference.
62    pub repairs: Vec<RepairCandidate>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum BorrowErrorKind {
67    /// Cannot borrow as mutable while shared borrow is active.
68    ConflictSharedExclusive,
69    /// Cannot borrow as mutable while another mutable borrow is active.
70    ConflictExclusiveExclusive,
71    /// Cannot read while exclusively borrowed.
72    ReadWhileExclusivelyBorrowed,
73    /// Cannot write while any borrow is active.
74    WriteWhileBorrowed,
75    /// Reference escapes its scope.
76    ReferenceEscape,
77    /// Use after move.
78    UseAfterMove,
79    /// Cannot share exclusive reference across task boundary.
80    ExclusiveRefAcrossTaskBoundary,
81}
82
83/// A repair candidate (fix suggestion) verified by re-running the solver.
84#[derive(Debug, Clone)]
85pub struct RepairCandidate {
86    pub kind: RepairKind,
87    /// Human-readable description of the fix.
88    pub description: String,
89    /// Concrete code diff (if available).
90    pub diff: Option<RepairDiff>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum RepairKind {
95    /// Reorder: move the conflicting statement after the last use of the blocking loan.
96    Reorder,
97    /// Scope: wrap the first borrow + its uses in a block `{ }`.
98    Scope,
99    /// Clone: suggest `clone x` instead of borrowing.
100    Clone,
101    /// Downgrade: change `&mut` to `&` if only reads exist.
102    Downgrade,
103    /// Extract: suggest extracting into a helper function.
104    Extract,
105}
106
107/// A concrete code change for a repair suggestion.
108#[derive(Debug, Clone)]
109pub struct RepairDiff {
110    /// Lines to remove (span + original text).
111    pub removals: Vec<(Span, String)>,
112    /// Lines to add (span + replacement text).
113    pub additions: Vec<(Span, String)>,
114}
115
116/// The ownership decision for an assignment of a non-Copy type.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum OwnershipDecision {
119    /// Move: source is dead after this point. Zero cost.
120    Move,
121    /// Clone: source is live after this point. Requires T: Clone.
122    Clone,
123    /// Copy: type is Copy (primitive). Trivially copied.
124    Copy,
125}
126
127/// Error for writing to an immutable binding.
128#[derive(Debug, Clone)]
129pub struct MutabilityError {
130    /// The span of the write attempt.
131    pub span: Span,
132    /// The name of the immutable variable.
133    pub variable_name: String,
134    /// The span of the original declaration.
135    pub declaration_span: Span,
136    /// Whether this is a `let` (explicit immutable) or `var` (inferred immutable).
137    pub is_explicit_let: bool,
138}
139
140impl BorrowAnalysis {
141    /// Create an empty analysis (used as default before solver runs).
142    pub fn empty() -> Self {
143        BorrowAnalysis {
144            liveness: LivenessResult {
145                live_in: HashMap::new(),
146                live_out: HashMap::new(),
147            },
148            loans_at_point: HashMap::new(),
149            loans: HashMap::new(),
150            errors: Vec::new(),
151            ownership_decisions: HashMap::new(),
152            mutability_errors: Vec::new(),
153        }
154    }
155
156    /// Check if the analysis found any errors.
157    pub fn has_errors(&self) -> bool {
158        !self.errors.is_empty() || !self.mutability_errors.is_empty()
159    }
160
161    /// Get the ownership decision for a given point.
162    /// Returns Copy for primitive types.
163    pub fn ownership_at(&self, point: Point) -> OwnershipDecision {
164        self.ownership_decisions
165            .get(&point)
166            .copied()
167            .unwrap_or(OwnershipDecision::Copy)
168    }
169
170    /// Get all active loans at a given point (for LSP borrow windows).
171    pub fn active_loans_at(&self, point: Point) -> &[LoanId] {
172        self.loans_at_point
173            .get(&point)
174            .map_or(&[], |v| v.as_slice())
175    }
176
177    /// Get loan info by ID.
178    pub fn loan(&self, id: LoanId) -> Option<&LoanInfo> {
179        self.loans.get(&id)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_empty_analysis() {
189        let analysis = BorrowAnalysis::empty();
190        assert!(!analysis.has_errors());
191        assert_eq!(analysis.ownership_at(Point(0)), OwnershipDecision::Copy);
192        assert!(analysis.active_loans_at(Point(0)).is_empty());
193    }
194}