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    /// Phase D: exclusive loan installed because a mutable capture targets a
42    /// non-escaping closure env whose storage plan is `LocalMutablePtr`. The
43    /// solver treats this exactly like any other exclusive loan — existing
44    /// `ConflictExclusiveExclusive` / `ReadWhileExclusivelyBorrowed` /
45    /// `WriteWhileBorrowed` rules catch races. Distinguished from `ClosureEnv`
46    /// so diagnostics and later phases can reason about it separately.
47    ClosureEnvMut,
48    ArrayStore,
49    ObjectStore,
50    EnumStore,
51    ArrayAssignment,
52    ObjectAssignment,
53    StructuredTaskBoundary,
54    DetachedTaskBoundary,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct LoanSink {
59    pub loan_id: u32,
60    pub kind: LoanSinkKind,
61    /// The slot that owns the sink when this is a closure or aggregate sink.
62    pub sink_slot: Option<SlotId>,
63    pub span: Span,
64}
65
66/// The complete borrow analysis for a single function.
67/// Produced by the Datafrog solver + liveness analysis.
68/// Consumed (read-only) by compiler, LSP, and diagnostics.
69#[derive(Debug, Clone)]
70pub struct BorrowAnalysis {
71    /// Liveness results for move/clone decisions.
72    pub liveness: LivenessResult,
73    /// Active loans at each program point (from Datafrog solver).
74    pub loans_at_point: HashMap<Point, Vec<LoanId>>,
75    /// Loan metadata.
76    pub loans: HashMap<LoanId, LoanInfo>,
77    /// Borrow errors detected by the solver.
78    pub errors: Vec<BorrowError>,
79    /// Move/clone decisions for each assignment of a non-Copy type.
80    pub ownership_decisions: HashMap<Point, OwnershipDecision>,
81    /// Immutability violations (writing to immutable bindings).
82    pub mutability_errors: Vec<MutabilityError>,
83    /// If this function safely returns one reference parameter (possibly with a
84    /// projection), records which parameter flows out and whether it is
85    /// shared/exclusive.
86    pub return_reference_summary: Option<ReturnReferenceSummary>,
87}
88
89/// Information about a single loan (borrow).
90#[derive(Debug, Clone)]
91pub struct LoanInfo {
92    pub id: LoanId,
93    /// The place being borrowed.
94    pub borrowed_place: Place,
95    /// Kind of borrow (shared or exclusive).
96    pub kind: BorrowKind,
97    /// Where the loan was issued.
98    pub issued_at: Point,
99    /// Source span of the borrow expression.
100    pub span: Span,
101    /// Nesting depth of the borrow's scope: 0 = parameter, 1 = function body local.
102    pub region_depth: u32,
103}
104
105/// A borrow conflict error with structured data for diagnostics.
106/// The diagnostic engine formats this; consumers never generate error text.
107#[derive(Debug, Clone)]
108pub struct BorrowError {
109    pub kind: BorrowErrorKind,
110    /// Primary span (the conflicting operation).
111    pub span: Span,
112    /// The loan that conflicts.
113    pub conflicting_loan: LoanId,
114    /// Where the conflicting loan was created.
115    pub loan_span: Span,
116    /// Where the loan is still needed (last use).
117    pub last_use_span: Option<Span>,
118    /// Repair candidates, ordered by preference.
119    pub repairs: Vec<RepairCandidate>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum BorrowErrorKind {
124    /// Cannot borrow as mutable while shared borrow is active.
125    ConflictSharedExclusive,
126    /// Cannot borrow as mutable while another mutable borrow is active.
127    ConflictExclusiveExclusive,
128    /// Cannot read while exclusively borrowed.
129    ReadWhileExclusivelyBorrowed,
130    /// Cannot write while any borrow is active.
131    WriteWhileBorrowed,
132    /// Reference escapes its scope.
133    ReferenceEscape,
134    /// Reference stored into an array.
135    ReferenceStoredInArray,
136    /// Reference stored into an object or struct literal.
137    ReferenceStoredInObject,
138    /// Reference stored into an enum payload.
139    ReferenceStoredInEnum,
140    /// Reference escapes into a closure environment.
141    ReferenceEscapeIntoClosure,
142    /// Use after move.
143    UseAfterMove,
144    /// Cannot share exclusive reference across task boundary.
145    ExclusiveRefAcrossTaskBoundary,
146    /// Cannot share any reference across detached task boundary.
147    SharedRefAcrossDetachedTask,
148    /// Reference returns must produce a reference on every path from the same
149    /// borrowed origin and borrow kind.
150    InconsistentReferenceReturn,
151    /// Two arguments at a call site alias the same variable but the callee
152    /// requires them to be non-aliased (one is mutated, the other is read).
153    CallSiteAliasConflict,
154    /// Non-sendable value (e.g., closure with mutable captures) sent across
155    /// a detached task boundary.
156    NonSendableAcrossTaskBoundary,
157}
158
159/// Stable, user-facing borrow error codes.
160///
161/// These provide a documented mapping from internal `BorrowErrorKind` variants
162/// to the `[B00XX]` codes shown in compiler and LSP diagnostics.  Both the
163/// lexical borrow checker (`borrow_checker.rs`) and the MIR-based checker use
164/// the same code space so users see consistent identifiers regardless of which
165/// analysis detected the problem.
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub enum BorrowErrorCode {
168    /// Borrow conflict (aliasing violation): shared+exclusive or exclusive+exclusive.
169    B0001,
170    /// Write to the owner while a borrow is active.
171    B0002,
172    /// Reference escapes its scope (return, store in collection, closure capture).
173    B0003,
174    /// Reference stored in a collection (array, object, enum).
175    B0004,
176    /// Use after move.
177    B0005,
178    /// Exclusive reference sent across a task/async boundary.
179    B0006,
180    /// Inconsistent return-reference summary across branches.
181    B0007,
182    /// Shared reference sent across a detached task boundary.
183    B0012,
184    /// Call-site alias conflict: same variable passed to conflicting parameters.
185    B0013,
186    /// Non-sendable value across detached task boundary.
187    B0014,
188}
189
190impl BorrowErrorCode {
191    /// The string form used in diagnostic messages, e.g. `"B0001"`.
192    pub fn as_str(self) -> &'static str {
193        match self {
194            BorrowErrorCode::B0001 => "B0001",
195            BorrowErrorCode::B0002 => "B0002",
196            BorrowErrorCode::B0003 => "B0003",
197            BorrowErrorCode::B0004 => "B0004",
198            BorrowErrorCode::B0005 => "B0005",
199            BorrowErrorCode::B0006 => "B0006",
200            BorrowErrorCode::B0007 => "B0007",
201            BorrowErrorCode::B0012 => "B0012",
202            BorrowErrorCode::B0013 => "B0013",
203            BorrowErrorCode::B0014 => "B0014",
204        }
205    }
206}
207
208impl std::fmt::Display for BorrowErrorCode {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        f.write_str(self.as_str())
211    }
212}
213
214impl BorrowErrorKind {
215    /// Map this error kind to the stable user-facing error code.
216    pub fn code(&self) -> BorrowErrorCode {
217        match self {
218            BorrowErrorKind::ConflictSharedExclusive
219            | BorrowErrorKind::ConflictExclusiveExclusive
220            | BorrowErrorKind::ReadWhileExclusivelyBorrowed => BorrowErrorCode::B0001,
221
222            BorrowErrorKind::WriteWhileBorrowed => BorrowErrorCode::B0002,
223
224            BorrowErrorKind::ReferenceEscape
225            | BorrowErrorKind::ReferenceEscapeIntoClosure => BorrowErrorCode::B0003,
226
227            BorrowErrorKind::ReferenceStoredInArray
228            | BorrowErrorKind::ReferenceStoredInObject
229            | BorrowErrorKind::ReferenceStoredInEnum => BorrowErrorCode::B0004,
230
231            BorrowErrorKind::UseAfterMove => BorrowErrorCode::B0005,
232
233            BorrowErrorKind::ExclusiveRefAcrossTaskBoundary => BorrowErrorCode::B0006,
234
235            BorrowErrorKind::SharedRefAcrossDetachedTask => BorrowErrorCode::B0012,
236
237            BorrowErrorKind::InconsistentReferenceReturn => BorrowErrorCode::B0007,
238
239            BorrowErrorKind::CallSiteAliasConflict => BorrowErrorCode::B0013,
240
241            BorrowErrorKind::NonSendableAcrossTaskBoundary => BorrowErrorCode::B0014,
242        }
243    }
244}
245
246/// A repair candidate (fix suggestion) verified by re-running the solver.
247#[derive(Debug, Clone)]
248pub struct RepairCandidate {
249    pub kind: RepairKind,
250    /// Human-readable description of the fix.
251    pub description: String,
252    /// Concrete code diff (if available).
253    pub diff: Option<RepairDiff>,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub enum RepairKind {
258    /// Reorder: move the conflicting statement after the last use of the blocking loan.
259    Reorder,
260    /// Scope: wrap the first borrow + its uses in a block `{ }`.
261    Scope,
262    /// Clone: suggest `clone x` instead of borrowing.
263    Clone,
264    /// Downgrade: change `&mut` to `&` if only reads exist.
265    Downgrade,
266    /// Extract: suggest extracting into a helper function.
267    Extract,
268}
269
270/// A concrete code change for a repair suggestion.
271#[derive(Debug, Clone)]
272pub struct RepairDiff {
273    /// Lines to remove (span + original text).
274    pub removals: Vec<(Span, String)>,
275    /// Lines to add (span + replacement text).
276    pub additions: Vec<(Span, String)>,
277}
278
279/// The ownership decision for an assignment of a non-Copy type.
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub enum OwnershipDecision {
282    /// Move: source is dead after this point. Zero cost.
283    Move,
284    /// Clone: source is live after this point. Requires T: Clone.
285    Clone,
286    /// Copy: type is Copy (primitive). Trivially copied.
287    Copy,
288}
289
290/// Summary of a function's parameter borrow requirements.
291/// Used for interprocedural alias checking at call sites.
292#[derive(Debug, Clone)]
293pub struct FunctionBorrowSummary {
294    /// Per-parameter borrow mode: None = owned, Some(Shared/Exclusive) = by reference.
295    pub param_borrows: Vec<Option<BorrowKind>>,
296    /// Pairs of parameter indices that must not alias (one is mutated, the other is read).
297    pub conflict_pairs: Vec<(usize, usize)>,
298    /// If the function returns a reference derived from a parameter, records which
299    /// parameter and borrow kind. Used for interprocedural composition.
300    pub return_summary: Option<ReturnReferenceSummary>,
301    /// Ownership classification of the function's return value (Phase 5.A).
302    /// Used by callers to skip unnecessary Arc→Box promotion when the callee
303    /// already returns a uniquely-owned value.
304    pub return_ownership_mode: ReturnOwnershipMode,
305    /// Per-parameter closure-escape bit (Closure Spec Phase B).
306    ///
307    /// `closure_param_escapes[i] == true` means the parameter at index `i` may
308    /// flow into one of the §2.1 escape vectors inside the function body
309    /// (returned, stored in a container, stored in a struct field, captured by
310    /// another closure, sent across a task boundary, passed through
311    /// `snapshot()`, written through a deref, or promoted to `UniqueHeap` /
312    /// `SharedCow` storage). `false` means the parameter is only used in
313    /// benign ways — read locally, called, or passed to a callee whose own
314    /// summary says the corresponding parameter is non-escaping.
315    ///
316    /// Length equals the number of function parameters. Used by the
317    /// storage-planning pass to decide whether a closure passed as a call-site
318    /// argument needs to be heap-allocated. The conservative default is
319    /// `true` for every parameter — Phase B only relaxes for clear,
320    /// intraprocedural cases; Phase C will tighten further once
321    /// monomorphization finalizes callee identity.
322    pub closure_param_escapes: Vec<bool>,
323}
324
325/// Ownership classification of a function's return value.
326///
327/// Used for interprocedural ownership inference (Phase 5.A). Each function's
328/// return is classified into one of these modes so that callers can propagate
329/// the right storage decisions without emitting redundant Arc→Box promotions.
330///
331/// `Unknown` is the conservative fallback — it preserves current Arc-everywhere
332/// behavior, so any inference uncertainty stays semantics-preserving.
333#[derive(
334    Debug,
335    Clone,
336    Copy,
337    PartialEq,
338    Eq,
339    Hash,
340    serde::Serialize,
341    serde::Deserialize,
342)]
343pub enum ReturnOwnershipMode {
344    /// Returns a newly-allocated owned value. Caller can take ownership directly
345    /// (as Box) without an Arc round-trip.
346    /// Example: `fn make() -> Array<int> { [1,2,3] }`
347    NewlyOwned,
348    /// Returns a reference/alias derived from the given parameter. Caller keeps
349    /// ownership of the source.
350    /// Example: `fn first(arr: &Array<int>) -> &int { &arr[0] }`
351    BorrowedFromParam(usize),
352    /// Returns a shared (Arc) value — reference-counted across callers.
353    Shared,
354    /// Returns a value proven to escape into global / static storage.
355    Static,
356    /// Could not infer — fall back to current Arc behavior.
357    Unknown,
358}
359
360impl ReturnOwnershipMode {
361    /// Combine two return modes observed on different return paths.
362    /// "Weakest" wins — any mismatch degrades to `Unknown` so the conservative
363    /// Arc fallback is always safe.
364    pub fn meet(self, other: Self) -> Self {
365        if self == other {
366            return self;
367        }
368        ReturnOwnershipMode::Unknown
369    }
370}
371
372impl Default for ReturnOwnershipMode {
373    fn default() -> Self {
374        ReturnOwnershipMode::Unknown
375    }
376}
377
378/// Error for writing to an immutable binding.
379#[derive(Debug, Clone)]
380pub struct MutabilityError {
381    /// The span of the write attempt.
382    pub span: Span,
383    /// The name of the immutable variable.
384    pub variable_name: String,
385    /// The span of the original declaration.
386    pub declaration_span: Span,
387    /// Whether this is an explicit immutable `let`.
388    pub is_explicit_let: bool,
389    /// Whether this is a `const` binding.
390    pub is_const: bool,
391}
392
393impl BorrowAnalysis {
394    /// Create an empty analysis (used as default before solver runs).
395    pub fn empty() -> Self {
396        BorrowAnalysis {
397            liveness: LivenessResult {
398                live_in: HashMap::new(),
399                live_out: HashMap::new(),
400            },
401            loans_at_point: HashMap::new(),
402            loans: HashMap::new(),
403            errors: Vec::new(),
404            ownership_decisions: HashMap::new(),
405            mutability_errors: Vec::new(),
406            return_reference_summary: None,
407        }
408    }
409
410    /// Check if the analysis found any errors.
411    pub fn has_errors(&self) -> bool {
412        !self.errors.is_empty() || !self.mutability_errors.is_empty()
413    }
414
415    /// Get the ownership decision for a given point.
416    /// Returns Copy for primitive types.
417    pub fn ownership_at(&self, point: Point) -> OwnershipDecision {
418        self.ownership_decisions
419            .get(&point)
420            .copied()
421            .unwrap_or(OwnershipDecision::Copy)
422    }
423
424    /// Get all active loans at a given point (for LSP borrow windows).
425    pub fn active_loans_at(&self, point: Point) -> &[LoanId] {
426        self.loans_at_point
427            .get(&point)
428            .map_or(&[], |v| v.as_slice())
429    }
430
431    /// Get loan info by ID.
432    pub fn loan(&self, id: LoanId) -> Option<&LoanInfo> {
433        self.loans.get(&id)
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_empty_analysis() {
443        let analysis = BorrowAnalysis::empty();
444        assert!(!analysis.has_errors());
445        assert_eq!(analysis.ownership_at(Point(0)), OwnershipDecision::Copy);
446        assert!(analysis.active_loans_at(Point(0)).is_empty());
447    }
448
449    // =========================================================================
450    // Error code mapping tests (Task 4)
451    // =========================================================================
452
453    #[test]
454    fn test_conflict_shared_exclusive_maps_to_b0001() {
455        assert_eq!(
456            BorrowErrorKind::ConflictSharedExclusive.code(),
457            BorrowErrorCode::B0001
458        );
459    }
460
461    #[test]
462    fn test_conflict_exclusive_exclusive_maps_to_b0001() {
463        assert_eq!(
464            BorrowErrorKind::ConflictExclusiveExclusive.code(),
465            BorrowErrorCode::B0001
466        );
467    }
468
469    #[test]
470    fn test_read_while_exclusively_borrowed_maps_to_b0001() {
471        assert_eq!(
472            BorrowErrorKind::ReadWhileExclusivelyBorrowed.code(),
473            BorrowErrorCode::B0001
474        );
475    }
476
477    #[test]
478    fn test_write_while_borrowed_maps_to_b0002() {
479        assert_eq!(
480            BorrowErrorKind::WriteWhileBorrowed.code(),
481            BorrowErrorCode::B0002
482        );
483    }
484
485    #[test]
486    fn test_reference_escape_maps_to_b0003() {
487        assert_eq!(
488            BorrowErrorKind::ReferenceEscape.code(),
489            BorrowErrorCode::B0003
490        );
491    }
492
493    #[test]
494    fn test_reference_escape_into_closure_maps_to_b0003() {
495        assert_eq!(
496            BorrowErrorKind::ReferenceEscapeIntoClosure.code(),
497            BorrowErrorCode::B0003
498        );
499    }
500
501    #[test]
502    fn test_reference_stored_in_array_maps_to_b0004() {
503        assert_eq!(
504            BorrowErrorKind::ReferenceStoredInArray.code(),
505            BorrowErrorCode::B0004
506        );
507    }
508
509    #[test]
510    fn test_reference_stored_in_object_maps_to_b0004() {
511        assert_eq!(
512            BorrowErrorKind::ReferenceStoredInObject.code(),
513            BorrowErrorCode::B0004
514        );
515    }
516
517    #[test]
518    fn test_reference_stored_in_enum_maps_to_b0004() {
519        assert_eq!(
520            BorrowErrorKind::ReferenceStoredInEnum.code(),
521            BorrowErrorCode::B0004
522        );
523    }
524
525    #[test]
526    fn test_use_after_move_maps_to_b0005() {
527        assert_eq!(
528            BorrowErrorKind::UseAfterMove.code(),
529            BorrowErrorCode::B0005
530        );
531    }
532
533    #[test]
534    fn test_exclusive_ref_across_task_boundary_maps_to_b0006() {
535        assert_eq!(
536            BorrowErrorKind::ExclusiveRefAcrossTaskBoundary.code(),
537            BorrowErrorCode::B0006
538        );
539    }
540
541    #[test]
542    fn test_inconsistent_reference_return_maps_to_b0007() {
543        assert_eq!(
544            BorrowErrorKind::InconsistentReferenceReturn.code(),
545            BorrowErrorCode::B0007
546        );
547    }
548
549    #[test]
550    fn test_borrow_error_code_as_str() {
551        assert_eq!(BorrowErrorCode::B0001.as_str(), "B0001");
552        assert_eq!(BorrowErrorCode::B0002.as_str(), "B0002");
553        assert_eq!(BorrowErrorCode::B0003.as_str(), "B0003");
554        assert_eq!(BorrowErrorCode::B0004.as_str(), "B0004");
555        assert_eq!(BorrowErrorCode::B0005.as_str(), "B0005");
556        assert_eq!(BorrowErrorCode::B0006.as_str(), "B0006");
557        assert_eq!(BorrowErrorCode::B0007.as_str(), "B0007");
558    }
559
560    #[test]
561    fn test_borrow_error_code_display() {
562        assert_eq!(format!("{}", BorrowErrorCode::B0001), "B0001");
563        assert_eq!(format!("{}", BorrowErrorCode::B0007), "B0007");
564    }
565
566    #[test]
567    fn test_all_error_kinds_have_codes() {
568        // Exhaustive check: every BorrowErrorKind variant must map to some code.
569        let all_kinds = vec![
570            BorrowErrorKind::ConflictSharedExclusive,
571            BorrowErrorKind::ConflictExclusiveExclusive,
572            BorrowErrorKind::ReadWhileExclusivelyBorrowed,
573            BorrowErrorKind::WriteWhileBorrowed,
574            BorrowErrorKind::ReferenceEscape,
575            BorrowErrorKind::ReferenceStoredInArray,
576            BorrowErrorKind::ReferenceStoredInObject,
577            BorrowErrorKind::ReferenceStoredInEnum,
578            BorrowErrorKind::ReferenceEscapeIntoClosure,
579            BorrowErrorKind::UseAfterMove,
580            BorrowErrorKind::ExclusiveRefAcrossTaskBoundary,
581            BorrowErrorKind::SharedRefAcrossDetachedTask,
582            BorrowErrorKind::InconsistentReferenceReturn,
583        ];
584        for kind in all_kinds {
585            // Should not panic — every variant is covered.
586            let _code = kind.code();
587        }
588    }
589}