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}