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}