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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct ReturnReferenceSummary {
17    pub param_index: usize,
18    pub kind: BorrowKind,
19    /// Exact projection chain when every successful return path agrees on it.
20    /// `None` means "same parameter root, but projection differs across paths".
21    pub projection: Option<Vec<ProjectionStep>>,
22}
23
24/// A normalized origin for a first-class reference value.
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct ReferenceOrigin {
27    pub root: ReferenceOriginRoot,
28    pub projection: Vec<ProjectionStep>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum ReferenceOriginRoot {
33    Param(usize),
34    Local(SlotId),
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum LoanSinkKind {
39    ReturnSlot,
40    ClosureEnv,
41    ArrayStore,
42    ObjectStore,
43    EnumStore,
44    ArrayAssignment,
45    ObjectAssignment,
46    StructuredTaskBoundary,
47    DetachedTaskBoundary,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51pub struct LoanSink {
52    pub loan_id: u32,
53    pub kind: LoanSinkKind,
54    /// The slot that owns the sink when this is a closure or aggregate sink.
55    pub sink_slot: Option<SlotId>,
56    pub span: Span,
57}
58
59/// The complete borrow analysis for a single function.
60/// Produced by the Datafrog solver + liveness analysis.
61/// Consumed (read-only) by compiler, LSP, and diagnostics.
62#[derive(Debug)]
63pub struct BorrowAnalysis {
64    /// Liveness results for move/clone decisions.
65    pub liveness: LivenessResult,
66    /// Active loans at each program point (from Datafrog solver).
67    pub loans_at_point: HashMap<Point, Vec<LoanId>>,
68    /// Loan metadata.
69    pub loans: HashMap<LoanId, LoanInfo>,
70    /// Borrow errors detected by the solver.
71    pub errors: Vec<BorrowError>,
72    /// Move/clone decisions for each assignment of a non-Copy type.
73    pub ownership_decisions: HashMap<Point, OwnershipDecision>,
74    /// Immutability violations (writing to immutable bindings).
75    pub mutability_errors: Vec<MutabilityError>,
76    /// If this function safely returns one reference parameter (possibly with a
77    /// projection), records which parameter flows out and whether it is
78    /// shared/exclusive.
79    pub return_reference_summary: Option<ReturnReferenceSummary>,
80}
81
82/// Information about a single loan (borrow).
83#[derive(Debug, Clone)]
84pub struct LoanInfo {
85    pub id: LoanId,
86    /// The place being borrowed.
87    pub borrowed_place: Place,
88    /// Kind of borrow (shared or exclusive).
89    pub kind: BorrowKind,
90    /// Where the loan was issued.
91    pub issued_at: Point,
92    /// Source span of the borrow expression.
93    pub span: Span,
94    /// Nesting depth of the borrow's scope: 0 = parameter, 1 = function body local.
95    pub region_depth: u32,
96}
97
98/// A borrow conflict error with structured data for diagnostics.
99/// The diagnostic engine formats this; consumers never generate error text.
100#[derive(Debug, Clone)]
101pub struct BorrowError {
102    pub kind: BorrowErrorKind,
103    /// Primary span (the conflicting operation).
104    pub span: Span,
105    /// The loan that conflicts.
106    pub conflicting_loan: LoanId,
107    /// Where the conflicting loan was created.
108    pub loan_span: Span,
109    /// Where the loan is still needed (last use).
110    pub last_use_span: Option<Span>,
111    /// Repair candidates, ordered by preference.
112    pub repairs: Vec<RepairCandidate>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum BorrowErrorKind {
117    /// Cannot borrow as mutable while shared borrow is active.
118    ConflictSharedExclusive,
119    /// Cannot borrow as mutable while another mutable borrow is active.
120    ConflictExclusiveExclusive,
121    /// Cannot read while exclusively borrowed.
122    ReadWhileExclusivelyBorrowed,
123    /// Cannot write while any borrow is active.
124    WriteWhileBorrowed,
125    /// Reference escapes its scope.
126    ReferenceEscape,
127    /// Reference stored into an array.
128    ReferenceStoredInArray,
129    /// Reference stored into an object or struct literal.
130    ReferenceStoredInObject,
131    /// Reference stored into an enum payload.
132    ReferenceStoredInEnum,
133    /// Reference escapes into a closure environment.
134    ReferenceEscapeIntoClosure,
135    /// Use after move.
136    UseAfterMove,
137    /// Cannot share exclusive reference across task boundary.
138    ExclusiveRefAcrossTaskBoundary,
139    /// Cannot share any reference across detached task boundary.
140    SharedRefAcrossDetachedTask,
141    /// Reference returns must produce a reference on every path from the same
142    /// borrowed origin and borrow kind.
143    InconsistentReferenceReturn,
144    /// Two arguments at a call site alias the same variable but the callee
145    /// requires them to be non-aliased (one is mutated, the other is read).
146    CallSiteAliasConflict,
147    /// Non-sendable value (e.g., closure with mutable captures) sent across
148    /// a detached task boundary.
149    NonSendableAcrossTaskBoundary,
150}
151
152/// Stable, user-facing borrow error codes.
153///
154/// These provide a documented mapping from internal `BorrowErrorKind` variants
155/// to the `[B00XX]` codes shown in compiler and LSP diagnostics.  Both the
156/// lexical borrow checker (`borrow_checker.rs`) and the MIR-based checker use
157/// the same code space so users see consistent identifiers regardless of which
158/// analysis detected the problem.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
160pub enum BorrowErrorCode {
161    /// Borrow conflict (aliasing violation): shared+exclusive or exclusive+exclusive.
162    B0001,
163    /// Write to the owner while a borrow is active.
164    B0002,
165    /// Reference escapes its scope (return, store in collection, closure capture).
166    B0003,
167    /// Reference stored in a collection (array, object, enum).
168    B0004,
169    /// Use after move.
170    B0005,
171    /// Exclusive reference sent across a task/async boundary.
172    B0006,
173    /// Inconsistent return-reference summary across branches.
174    B0007,
175    /// Shared reference sent across a detached task boundary.
176    B0012,
177    /// Call-site alias conflict: same variable passed to conflicting parameters.
178    B0013,
179    /// Non-sendable value across detached task boundary.
180    B0014,
181}
182
183impl BorrowErrorCode {
184    /// The string form used in diagnostic messages, e.g. `"B0001"`.
185    pub fn as_str(self) -> &'static str {
186        match self {
187            BorrowErrorCode::B0001 => "B0001",
188            BorrowErrorCode::B0002 => "B0002",
189            BorrowErrorCode::B0003 => "B0003",
190            BorrowErrorCode::B0004 => "B0004",
191            BorrowErrorCode::B0005 => "B0005",
192            BorrowErrorCode::B0006 => "B0006",
193            BorrowErrorCode::B0007 => "B0007",
194            BorrowErrorCode::B0012 => "B0012",
195            BorrowErrorCode::B0013 => "B0013",
196            BorrowErrorCode::B0014 => "B0014",
197        }
198    }
199}
200
201impl std::fmt::Display for BorrowErrorCode {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        f.write_str(self.as_str())
204    }
205}
206
207impl BorrowErrorKind {
208    /// Map this error kind to the stable user-facing error code.
209    pub fn code(&self) -> BorrowErrorCode {
210        match self {
211            BorrowErrorKind::ConflictSharedExclusive
212            | BorrowErrorKind::ConflictExclusiveExclusive
213            | BorrowErrorKind::ReadWhileExclusivelyBorrowed => BorrowErrorCode::B0001,
214
215            BorrowErrorKind::WriteWhileBorrowed => BorrowErrorCode::B0002,
216
217            BorrowErrorKind::ReferenceEscape
218            | BorrowErrorKind::ReferenceEscapeIntoClosure => BorrowErrorCode::B0003,
219
220            BorrowErrorKind::ReferenceStoredInArray
221            | BorrowErrorKind::ReferenceStoredInObject
222            | BorrowErrorKind::ReferenceStoredInEnum => BorrowErrorCode::B0004,
223
224            BorrowErrorKind::UseAfterMove => BorrowErrorCode::B0005,
225
226            BorrowErrorKind::ExclusiveRefAcrossTaskBoundary => BorrowErrorCode::B0006,
227
228            BorrowErrorKind::SharedRefAcrossDetachedTask => BorrowErrorCode::B0012,
229
230            BorrowErrorKind::InconsistentReferenceReturn => BorrowErrorCode::B0007,
231
232            BorrowErrorKind::CallSiteAliasConflict => BorrowErrorCode::B0013,
233
234            BorrowErrorKind::NonSendableAcrossTaskBoundary => BorrowErrorCode::B0014,
235        }
236    }
237}
238
239/// A repair candidate (fix suggestion) verified by re-running the solver.
240#[derive(Debug, Clone)]
241pub struct RepairCandidate {
242    pub kind: RepairKind,
243    /// Human-readable description of the fix.
244    pub description: String,
245    /// Concrete code diff (if available).
246    pub diff: Option<RepairDiff>,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub enum RepairKind {
251    /// Reorder: move the conflicting statement after the last use of the blocking loan.
252    Reorder,
253    /// Scope: wrap the first borrow + its uses in a block `{ }`.
254    Scope,
255    /// Clone: suggest `clone x` instead of borrowing.
256    Clone,
257    /// Downgrade: change `&mut` to `&` if only reads exist.
258    Downgrade,
259    /// Extract: suggest extracting into a helper function.
260    Extract,
261}
262
263/// A concrete code change for a repair suggestion.
264#[derive(Debug, Clone)]
265pub struct RepairDiff {
266    /// Lines to remove (span + original text).
267    pub removals: Vec<(Span, String)>,
268    /// Lines to add (span + replacement text).
269    pub additions: Vec<(Span, String)>,
270}
271
272/// The ownership decision for an assignment of a non-Copy type.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum OwnershipDecision {
275    /// Move: source is dead after this point. Zero cost.
276    Move,
277    /// Clone: source is live after this point. Requires T: Clone.
278    Clone,
279    /// Copy: type is Copy (primitive). Trivially copied.
280    Copy,
281}
282
283/// Summary of a function's parameter borrow requirements.
284/// Used for interprocedural alias checking at call sites.
285#[derive(Debug, Clone)]
286pub struct FunctionBorrowSummary {
287    /// Per-parameter borrow mode: None = owned, Some(Shared/Exclusive) = by reference.
288    pub param_borrows: Vec<Option<BorrowKind>>,
289    /// Pairs of parameter indices that must not alias (one is mutated, the other is read).
290    pub conflict_pairs: Vec<(usize, usize)>,
291    /// If the function returns a reference derived from a parameter, records which
292    /// parameter and borrow kind. Used for interprocedural composition.
293    pub return_summary: Option<ReturnReferenceSummary>,
294}
295
296/// Error for writing to an immutable binding.
297#[derive(Debug, Clone)]
298pub struct MutabilityError {
299    /// The span of the write attempt.
300    pub span: Span,
301    /// The name of the immutable variable.
302    pub variable_name: String,
303    /// The span of the original declaration.
304    pub declaration_span: Span,
305    /// Whether this is an explicit immutable `let`.
306    pub is_explicit_let: bool,
307    /// Whether this is a `const` binding.
308    pub is_const: bool,
309}
310
311impl BorrowAnalysis {
312    /// Create an empty analysis (used as default before solver runs).
313    pub fn empty() -> Self {
314        BorrowAnalysis {
315            liveness: LivenessResult {
316                live_in: HashMap::new(),
317                live_out: HashMap::new(),
318            },
319            loans_at_point: HashMap::new(),
320            loans: HashMap::new(),
321            errors: Vec::new(),
322            ownership_decisions: HashMap::new(),
323            mutability_errors: Vec::new(),
324            return_reference_summary: None,
325        }
326    }
327
328    /// Check if the analysis found any errors.
329    pub fn has_errors(&self) -> bool {
330        !self.errors.is_empty() || !self.mutability_errors.is_empty()
331    }
332
333    /// Get the ownership decision for a given point.
334    /// Returns Copy for primitive types.
335    pub fn ownership_at(&self, point: Point) -> OwnershipDecision {
336        self.ownership_decisions
337            .get(&point)
338            .copied()
339            .unwrap_or(OwnershipDecision::Copy)
340    }
341
342    /// Get all active loans at a given point (for LSP borrow windows).
343    pub fn active_loans_at(&self, point: Point) -> &[LoanId] {
344        self.loans_at_point
345            .get(&point)
346            .map_or(&[], |v| v.as_slice())
347    }
348
349    /// Get loan info by ID.
350    pub fn loan(&self, id: LoanId) -> Option<&LoanInfo> {
351        self.loans.get(&id)
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_empty_analysis() {
361        let analysis = BorrowAnalysis::empty();
362        assert!(!analysis.has_errors());
363        assert_eq!(analysis.ownership_at(Point(0)), OwnershipDecision::Copy);
364        assert!(analysis.active_loans_at(Point(0)).is_empty());
365    }
366
367    // =========================================================================
368    // Error code mapping tests (Task 4)
369    // =========================================================================
370
371    #[test]
372    fn test_conflict_shared_exclusive_maps_to_b0001() {
373        assert_eq!(
374            BorrowErrorKind::ConflictSharedExclusive.code(),
375            BorrowErrorCode::B0001
376        );
377    }
378
379    #[test]
380    fn test_conflict_exclusive_exclusive_maps_to_b0001() {
381        assert_eq!(
382            BorrowErrorKind::ConflictExclusiveExclusive.code(),
383            BorrowErrorCode::B0001
384        );
385    }
386
387    #[test]
388    fn test_read_while_exclusively_borrowed_maps_to_b0001() {
389        assert_eq!(
390            BorrowErrorKind::ReadWhileExclusivelyBorrowed.code(),
391            BorrowErrorCode::B0001
392        );
393    }
394
395    #[test]
396    fn test_write_while_borrowed_maps_to_b0002() {
397        assert_eq!(
398            BorrowErrorKind::WriteWhileBorrowed.code(),
399            BorrowErrorCode::B0002
400        );
401    }
402
403    #[test]
404    fn test_reference_escape_maps_to_b0003() {
405        assert_eq!(
406            BorrowErrorKind::ReferenceEscape.code(),
407            BorrowErrorCode::B0003
408        );
409    }
410
411    #[test]
412    fn test_reference_escape_into_closure_maps_to_b0003() {
413        assert_eq!(
414            BorrowErrorKind::ReferenceEscapeIntoClosure.code(),
415            BorrowErrorCode::B0003
416        );
417    }
418
419    #[test]
420    fn test_reference_stored_in_array_maps_to_b0004() {
421        assert_eq!(
422            BorrowErrorKind::ReferenceStoredInArray.code(),
423            BorrowErrorCode::B0004
424        );
425    }
426
427    #[test]
428    fn test_reference_stored_in_object_maps_to_b0004() {
429        assert_eq!(
430            BorrowErrorKind::ReferenceStoredInObject.code(),
431            BorrowErrorCode::B0004
432        );
433    }
434
435    #[test]
436    fn test_reference_stored_in_enum_maps_to_b0004() {
437        assert_eq!(
438            BorrowErrorKind::ReferenceStoredInEnum.code(),
439            BorrowErrorCode::B0004
440        );
441    }
442
443    #[test]
444    fn test_use_after_move_maps_to_b0005() {
445        assert_eq!(
446            BorrowErrorKind::UseAfterMove.code(),
447            BorrowErrorCode::B0005
448        );
449    }
450
451    #[test]
452    fn test_exclusive_ref_across_task_boundary_maps_to_b0006() {
453        assert_eq!(
454            BorrowErrorKind::ExclusiveRefAcrossTaskBoundary.code(),
455            BorrowErrorCode::B0006
456        );
457    }
458
459    #[test]
460    fn test_inconsistent_reference_return_maps_to_b0007() {
461        assert_eq!(
462            BorrowErrorKind::InconsistentReferenceReturn.code(),
463            BorrowErrorCode::B0007
464        );
465    }
466
467    #[test]
468    fn test_borrow_error_code_as_str() {
469        assert_eq!(BorrowErrorCode::B0001.as_str(), "B0001");
470        assert_eq!(BorrowErrorCode::B0002.as_str(), "B0002");
471        assert_eq!(BorrowErrorCode::B0003.as_str(), "B0003");
472        assert_eq!(BorrowErrorCode::B0004.as_str(), "B0004");
473        assert_eq!(BorrowErrorCode::B0005.as_str(), "B0005");
474        assert_eq!(BorrowErrorCode::B0006.as_str(), "B0006");
475        assert_eq!(BorrowErrorCode::B0007.as_str(), "B0007");
476    }
477
478    #[test]
479    fn test_borrow_error_code_display() {
480        assert_eq!(format!("{}", BorrowErrorCode::B0001), "B0001");
481        assert_eq!(format!("{}", BorrowErrorCode::B0007), "B0007");
482    }
483
484    #[test]
485    fn test_all_error_kinds_have_codes() {
486        // Exhaustive check: every BorrowErrorKind variant must map to some code.
487        let all_kinds = vec![
488            BorrowErrorKind::ConflictSharedExclusive,
489            BorrowErrorKind::ConflictExclusiveExclusive,
490            BorrowErrorKind::ReadWhileExclusivelyBorrowed,
491            BorrowErrorKind::WriteWhileBorrowed,
492            BorrowErrorKind::ReferenceEscape,
493            BorrowErrorKind::ReferenceStoredInArray,
494            BorrowErrorKind::ReferenceStoredInObject,
495            BorrowErrorKind::ReferenceStoredInEnum,
496            BorrowErrorKind::ReferenceEscapeIntoClosure,
497            BorrowErrorKind::UseAfterMove,
498            BorrowErrorKind::ExclusiveRefAcrossTaskBoundary,
499            BorrowErrorKind::SharedRefAcrossDetachedTask,
500            BorrowErrorKind::InconsistentReferenceReturn,
501        ];
502        for kind in all_kinds {
503            // Should not panic — every variant is covered.
504            let _code = kind.code();
505        }
506    }
507}