Skip to main content

shape_vm/mir/
solver.rs

1//! Datafrog-based NLL borrow solver.
2//!
3//! Implements Non-Lexical Lifetimes using Datafrog's monotone fixed-point engine.
4//! This is the core of Shape's borrow checking — it determines which borrows are
5//! alive at each program point and detects conflicts.
6//!
7//! **Single source of truth**: This solver produces `BorrowAnalysis`, which is
8//! consumed by the compiler, LSP, and diagnostic engine. No consumer re-derives results.
9//!
10//! ## The Datafrog pattern
11//!
12//! [Datafrog](https://crates.io/crates/datafrog) is a lightweight Datalog engine
13//! that computes fixed points over monotone relations. The pattern used here is:
14//!
15//! 1. **Define input relations** — static facts extracted from MIR that never
16//!    change during iteration (e.g. `cfg_edge`, `invalidates`).
17//! 2. **Define derived variables** — monotonically-growing sets computed by
18//!    Datafrog's iteration engine (e.g. `loan_live_at`).
19//! 3. **Seed** the derived variable with initial facts (each loan is live at
20//!    its issuance point).
21//! 4. **Express rules** as `from_leapjoin` calls inside a `while iteration.changed()`
22//!    loop. Each rule joins a derived variable against input relations and
23//!    produces new tuples. Datafrog deduplicates and tracks whether any new
24//!    tuples were added (the `changed()` check).
25//! 5. **Convergence**: Because all relations are sets of tuples and rules only
26//!    add (never remove), the iteration terminates when no new tuples are
27//!    produced — the monotone fixed point.
28//! 6. **Post-processing**: After convergence, the derived relation is
29//!    `.complete()`-d into a frozen `Relation` and scanned for error conditions.
30//!
31//! ## Input relations (populated from MIR)
32//!
33//!   - `loan_issued_at(Loan, Point)` — a borrow was created
34//!   - `cfg_edge(Point, Point)` — control flow between points
35//!   - `invalidates(Point, Loan)` — an action invalidates a loan
36//!   - `use_of_loan(Loan, Point)` — a loan is used (the ref is read/used)
37//!
38//! ## Derived relations (Datafrog fixpoint)
39//!
40//!   - `loan_live_at(Loan, Point)` — a loan is still active
41//!   - `error(Point, Loan, Loan)` — two conflicting loans are simultaneously active
42//!
43//! ## Additional analyses
44//!
45//! - **Post-solve relaxation**: `solve()` skips `ReferenceStoredIn*` errors
46//!   when the container slot's `EscapeStatus` is `Local` (never escapes).
47//! - **Interprocedural summaries**: `extract_borrow_summary()` derives per-function
48//!   conflict pairs for call-site alias checking.
49//! - **Task-boundary sendability**: Detects closures with mutable captures
50//!   crossing detached task boundaries (B0014).
51
52use super::analysis::*;
53use super::cfg::ControlFlowGraph;
54use super::liveness::{self, LivenessResult};
55use super::types::*;
56use crate::type_tracking::EscapeStatus;
57use datafrog::{Iteration, Relation, RelationLeaper};
58use std::collections::{HashMap, HashSet};
59
60/// Callee return-reference summaries, keyed by function name.
61pub type CalleeSummaries = HashMap<String, ReturnReferenceSummary>;
62
63/// Input facts extracted from MIR for the Datafrog solver.
64#[derive(Debug, Default)]
65pub struct BorrowFacts {
66    /// (loan_id, point) — loan was created at this point
67    pub loan_issued_at: Vec<(u32, u32)>,
68    /// (from_point, to_point) — control flow edge
69    pub cfg_edge: Vec<(u32, u32)>,
70    /// (point, loan_id) — this point invalidates the loan (drop, reassignment)
71    pub invalidates: Vec<(u32, u32)>,
72    /// (loan_id, point) — the loan (reference) is used at this point
73    pub use_of_loan: Vec<(u32, u32)>,
74    /// Source span for each statement point.
75    pub point_spans: HashMap<u32, shape_ast::ast::Span>,
76    /// Loan metadata for error reporting.
77    pub loan_info: HashMap<u32, LoanInfo>,
78    /// Points where two loans conflict (same place, incompatible borrows).
79    pub potential_conflicts: Vec<(u32, u32)>, // (loan_a, loan_b)
80    /// Writes that may conflict with active loans: (point, place, span).
81    pub writes: Vec<(u32, Place, shape_ast::ast::Span)>,
82    /// Reads from owner places that may conflict with active exclusive loans.
83    pub reads: Vec<(u32, Place, shape_ast::ast::Span)>,
84    /// Escape classification for every local slot in the MIR function.
85    pub slot_escape_status: HashMap<SlotId, EscapeStatus>,
86    /// Loans that flow into the dedicated return slot and would escape.
87    pub escaped_loans: Vec<(u32, shape_ast::ast::Span)>,
88    /// Unified sink records for all loan escapes/stores/boundaries.
89    pub loan_sinks: Vec<LoanSink>,
90    /// Exclusive loans captured across an async/task boundary.
91    pub task_boundary_loans: Vec<(u32, shape_ast::ast::Span)>,
92    /// Loans captured into a closure environment.
93    pub closure_capture_loans: Vec<(u32, shape_ast::ast::Span)>,
94    /// Loans stored into array literals.
95    pub array_store_loans: Vec<(u32, shape_ast::ast::Span)>,
96    /// Loans stored into object/struct literals.
97    pub object_store_loans: Vec<(u32, shape_ast::ast::Span)>,
98    /// Loans stored into enum payloads.
99    pub enum_store_loans: Vec<(u32, shape_ast::ast::Span)>,
100    /// Loans written through field assignments into aggregate places.
101    pub object_assignment_loans: Vec<(u32, shape_ast::ast::Span)>,
102    /// Loans written through index assignments into aggregate places.
103    pub array_assignment_loans: Vec<(u32, shape_ast::ast::Span)>,
104    /// Reference-return summaries flowing into the return slot.
105    pub return_reference_candidates: Vec<(ReturnReferenceSummary, shape_ast::ast::Span)>,
106    /// Return-slot writes that produce a plain owned value.
107    pub non_reference_return_spans: Vec<shape_ast::ast::Span>,
108    /// Non-sendable values crossing detached task boundaries (e.g., closures
109    /// with mutable captures).
110    pub non_sendable_task_boundary: Vec<(u32, shape_ast::ast::Span)>,
111}
112
113/// Populate borrow facts from a MIR function and its CFG.
114pub fn extract_facts(
115    mir: &MirFunction,
116    cfg: &ControlFlowGraph,
117    callee_summaries: &CalleeSummaries,
118) -> BorrowFacts {
119    let mut facts = BorrowFacts::default();
120    let mut next_loan = 0u32;
121    let mut slot_loans: HashMap<SlotId, Vec<u32>> = HashMap::new();
122    let mut slot_reference_origins: HashMap<SlotId, (BorrowKind, ReferenceOrigin)> =
123        HashMap::new();
124
125    // Track slots that are targets of ClosureCapture with mutable captures
126    // (proxy for non-sendable closures).
127    let (all_captures, mutable_captures) =
128        super::storage_planning::collect_closure_captures(mir);
129    let closure_capture_slots: HashSet<SlotId> = mutable_captures;
130    facts.slot_escape_status.extend((0..mir.num_locals).map(|raw_slot| {
131        let slot = SlotId(raw_slot);
132        (
133            slot,
134            super::storage_planning::detect_escape_status(slot, mir, &all_captures),
135        )
136    }));
137    let param_reference_summaries: HashMap<SlotId, ReturnReferenceSummary> = mir
138        .param_slots
139        .iter()
140        .enumerate()
141        .filter_map(|(param_index, slot)| {
142            mir.param_reference_kinds
143                .get(param_index)
144                .copied()
145                .flatten()
146                .map(|kind| {
147                    (
148                        *slot,
149                        ReturnReferenceSummary {
150                            param_index,
151                            kind,
152                            projection: Some(Vec::new()),
153                        },
154                    )
155                })
156        })
157        .collect();
158    let mut slot_reference_summaries = param_reference_summaries.clone();
159
160    // Extract CFG edges from the block structure
161    for block in &mir.blocks {
162        // Edges between consecutive statements within a block
163        for i in 0..block.statements.len().saturating_sub(1) {
164            let from = block.statements[i].point.0;
165            let to = block.statements[i + 1].point.0;
166            facts.cfg_edge.push((from, to));
167        }
168
169        // Edge from last statement to successor blocks' first statements
170        let last_point = block.statements.last().map(|s| s.point.0).unwrap_or(0);
171
172        for &succ_id in cfg.successors(block.id) {
173            let succ_block = mir.block(succ_id);
174            if let Some(first_stmt) = succ_block.statements.first() {
175                facts.cfg_edge.push((last_point, first_stmt.point.0));
176            }
177        }
178    }
179
180    // Extract loan facts from statements
181    for block in &mir.blocks {
182        for stmt in &block.statements {
183            facts.point_spans.insert(stmt.point.0, stmt.span);
184            match &stmt.kind {
185                StatementKind::Assign(dest, Rvalue::Borrow(kind, place)) => {
186                    let loan_id = next_loan;
187                    next_loan += 1;
188
189                    facts.loan_issued_at.push((loan_id, stmt.point.0));
190                    if let Place::Local(slot) = dest {
191                        slot_loans.insert(*slot, vec![loan_id]);
192                        slot_reference_origins.insert(
193                            *slot,
194                            (*kind, reference_origin_for_place(place, &mir.param_slots)),
195                        );
196                        if let Some(contract) = safe_reference_summary_for_borrow(
197                            *kind,
198                            place,
199                            &param_reference_summaries,
200                        ) {
201                            slot_reference_summaries.insert(*slot, contract);
202                        } else {
203                            slot_reference_summaries.remove(slot);
204                        }
205                        if *slot == SlotId(0) {
206                            if let Some(contract) = safe_reference_summary_for_borrow(
207                                *kind,
208                                place,
209                                &param_reference_summaries,
210                            ) {
211                                facts
212                                    .return_reference_candidates
213                                    .push((contract, stmt.span));
214                            } else {
215                                facts.escaped_loans.push((loan_id, stmt.span));
216                                facts.loan_sinks.push(LoanSink {
217                                    loan_id,
218                                    kind: LoanSinkKind::ReturnSlot,
219                                    sink_slot: Some(*slot),
220                                    span: stmt.span,
221                                });
222                            }
223                        }
224                    }
225                    // Compute region depth: parameter loans get 0, locals get 1.
226                    let region_depth = if mir.param_slots.contains(&place.root_local()) {
227                        0 // Parameter — lives for the entire function
228                    } else {
229                        1 // Local — lives within the function body
230                    };
231                    facts.loan_info.insert(
232                        loan_id,
233                        LoanInfo {
234                            id: LoanId(loan_id),
235                            borrowed_place: place.clone(),
236                            kind: *kind,
237                            issued_at: stmt.point,
238                            span: stmt.span,
239                            region_depth,
240                        },
241                    );
242                }
243                StatementKind::Assign(place, rvalue) => {
244                    if let Place::Local(dest_slot) = place {
245                        update_slot_loan_aliases(&mut slot_loans, *dest_slot, rvalue);
246                        update_slot_reference_origins(
247                            &mut slot_reference_origins,
248                            *dest_slot,
249                            rvalue,
250                        );
251                        update_slot_reference_summaries(
252                            &mut slot_reference_summaries,
253                            *dest_slot,
254                            rvalue,
255                        );
256                        if *dest_slot == SlotId(0) {
257                            let mut found_reference_return = false;
258                            if let Some(contract) =
259                                reference_summary_from_rvalue(&slot_reference_summaries, rvalue)
260                            {
261                                facts
262                                    .return_reference_candidates
263                                    .push((contract, stmt.span));
264                                found_reference_return = true;
265                            }
266                            if let Some((borrow_kind, origin)) =
267                                reference_origin_from_rvalue(&slot_reference_origins, rvalue)
268                            {
269                                if let Some(contract) =
270                                    reference_summary_from_origin(borrow_kind, &origin)
271                                {
272                                    facts
273                                        .return_reference_candidates
274                                        .push((contract, stmt.span));
275                                    found_reference_return = true;
276                                }
277                            }
278                            for loan_id in local_loans_from_rvalue(&slot_loans, rvalue) {
279                                let info = &facts.loan_info[&loan_id];
280                                if let Some(contract) = safe_reference_summary_for_borrow(
281                                    info.kind,
282                                    &info.borrowed_place,
283                                    &param_reference_summaries,
284                                ) {
285                                    facts
286                                        .return_reference_candidates
287                                        .push((contract, stmt.span));
288                                    found_reference_return = true;
289                                } else {
290                                    facts.escaped_loans.push((loan_id, stmt.span));
291                                    facts.loan_sinks.push(LoanSink {
292                                        loan_id,
293                                        kind: LoanSinkKind::ReturnSlot,
294                                        sink_slot: Some(*dest_slot),
295                                        span: stmt.span,
296                                    });
297                                }
298                            }
299                            if !found_reference_return {
300                                facts.non_reference_return_spans.push(stmt.span);
301                            }
302                        }
303                    }
304                    match place {
305                        Place::Field(..) => {
306                            for loan_id in local_loans_from_rvalue(&slot_loans, rvalue) {
307                                facts.object_assignment_loans.push((loan_id, stmt.span));
308                                facts.loan_sinks.push(LoanSink {
309                                    loan_id,
310                                    kind: LoanSinkKind::ObjectAssignment,
311                                    sink_slot: Some(place.root_local()),
312                                    span: stmt.span,
313                                });
314                            }
315                        }
316                        Place::Index(..) => {
317                            for loan_id in local_loans_from_rvalue(&slot_loans, rvalue) {
318                                facts.array_assignment_loans.push((loan_id, stmt.span));
319                                facts.loan_sinks.push(LoanSink {
320                                    loan_id,
321                                    kind: LoanSinkKind::ArrayAssignment,
322                                    sink_slot: Some(place.root_local()),
323                                    span: stmt.span,
324                                });
325                            }
326                        }
327                        Place::Local(..) | Place::Deref(..) => {}
328                    }
329                    facts.writes.push((stmt.point.0, place.clone(), stmt.span));
330                    // Assignment to a place invalidates all loans on that place
331                    for (lid, info) in &facts.loan_info {
332                        if place.conflicts_with(&info.borrowed_place) {
333                            facts.invalidates.push((stmt.point.0, *lid));
334                        }
335                    }
336                }
337                StatementKind::Drop(place) => {
338                    // Drop invalidates all loans on the place
339                    for (lid, info) in &facts.loan_info {
340                        if place.conflicts_with(&info.borrowed_place) {
341                            facts.invalidates.push((stmt.point.0, *lid));
342                        }
343                    }
344                }
345                StatementKind::TaskBoundary(operands, kind) => {
346                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
347                        let info = &facts.loan_info[&loan_id];
348                        match kind {
349                            TaskBoundaryKind::Detached => {
350                                // All refs (shared + exclusive) rejected across detached tasks
351                                facts.task_boundary_loans.push((loan_id, stmt.span));
352                                facts.loan_sinks.push(LoanSink {
353                                    loan_id,
354                                    kind: LoanSinkKind::DetachedTaskBoundary,
355                                    sink_slot: None,
356                                    span: stmt.span,
357                                });
358                            }
359                            TaskBoundaryKind::Structured => {
360                                // Only exclusive refs rejected across structured tasks
361                                if info.kind == BorrowKind::Exclusive {
362                                    facts.task_boundary_loans.push((loan_id, stmt.span));
363                                    facts.loan_sinks.push(LoanSink {
364                                        loan_id,
365                                        kind: LoanSinkKind::StructuredTaskBoundary,
366                                        sink_slot: None,
367                                        span: stmt.span,
368                                    });
369                                }
370                            }
371                        }
372                    }
373                    // Sendability check for detached tasks: closures with mutable
374                    // captures are not sendable across detached boundaries.
375                    if *kind == TaskBoundaryKind::Detached {
376                        for op in operands {
377                            if let Operand::Copy(Place::Local(slot))
378                            | Operand::Move(Place::Local(slot)) = op
379                            {
380                                if closure_capture_slots.contains(slot) {
381                                    facts
382                                        .non_sendable_task_boundary
383                                        .push((slot.0 as u32, stmt.span));
384                                }
385                            }
386                        }
387                    }
388                }
389                StatementKind::ClosureCapture {
390                    closure_slot,
391                    operands,
392                } => {
393                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
394                        facts.closure_capture_loans.push((loan_id, stmt.span));
395                        facts.loan_sinks.push(LoanSink {
396                            loan_id,
397                            kind: LoanSinkKind::ClosureEnv,
398                            sink_slot: Some(*closure_slot),
399                            span: stmt.span,
400                        });
401                    }
402                }
403                StatementKind::ArrayStore {
404                    container_slot,
405                    operands,
406                } => {
407                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
408                        facts.array_store_loans.push((loan_id, stmt.span));
409                        facts.loan_sinks.push(LoanSink {
410                            loan_id,
411                            kind: LoanSinkKind::ArrayStore,
412                            sink_slot: Some(*container_slot),
413                            span: stmt.span,
414                        });
415                    }
416                }
417                StatementKind::ObjectStore {
418                    container_slot,
419                    operands,
420                } => {
421                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
422                        facts.object_store_loans.push((loan_id, stmt.span));
423                        facts.loan_sinks.push(LoanSink {
424                            loan_id,
425                            kind: LoanSinkKind::ObjectStore,
426                            sink_slot: Some(*container_slot),
427                            span: stmt.span,
428                        });
429                    }
430                }
431                StatementKind::EnumStore {
432                    container_slot,
433                    operands,
434                } => {
435                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
436                        facts.enum_store_loans.push((loan_id, stmt.span));
437                        facts.loan_sinks.push(LoanSink {
438                            loan_id,
439                            kind: LoanSinkKind::EnumStore,
440                            sink_slot: Some(*container_slot),
441                            span: stmt.span,
442                        });
443                    }
444                }
445                StatementKind::Nop => {}
446            }
447
448            for read_place in statement_read_places(&stmt.kind) {
449                facts
450                    .reads
451                    .push((stmt.point.0, read_place.clone(), stmt.span));
452                if let Place::Local(slot) = read_place {
453                    if let Some(loans) = slot_loans.get(&slot) {
454                        for loan_id in loans {
455                            facts.use_of_loan.push((*loan_id, stmt.point.0));
456                        }
457                    }
458                }
459            }
460        }
461
462        // Process Call terminators for borrow facts
463        if let TerminatorKind::Call { func, args, destination, .. } = &block.terminator.kind {
464            let call_point = block.statements.last().map(|s| s.point.0).unwrap_or(0);
465            // Track reads from func and args operands
466            let mut all_operands = vec![func];
467            all_operands.extend(args.iter());
468            for op in &all_operands {
469                if let Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) = op {
470                    if let Some(loans) = slot_loans.get(&place.root_local()) {
471                        for &loan_id in loans {
472                            facts.use_of_loan.push((loan_id, call_point));
473                        }
474                    }
475                }
476            }
477            // Destination write: clear provenance, then compose callee summary if available
478            let dest_slot = destination.root_local();
479            slot_loans.remove(&dest_slot);
480            slot_reference_origins.remove(&dest_slot);
481            slot_reference_summaries.remove(&dest_slot);
482
483            // Compose callee return summary into destination slot (summary-driven).
484            // Only compose for MirConstant::Function calls — indirect calls (closures,
485            // method dispatch) use conservative clearing.
486            if let Operand::Constant(MirConstant::Function(callee_name)) = func {
487                if let Some(callee_summary) = callee_summaries.get(callee_name.as_str()) {
488                    if let Some(arg_operand) = args.get(callee_summary.param_index) {
489                        if let Operand::Copy(arg_place)
490                        | Operand::Move(arg_place)
491                        | Operand::MoveExplicit(arg_place) = arg_operand
492                        {
493                            let arg_slot = arg_place.root_local();
494
495                            // Inherit loans from the argument slot
496                            if let Some(arg_loans) = slot_loans.get(&arg_slot).cloned() {
497                                slot_loans.insert(dest_slot, arg_loans);
498                            }
499
500                            // Compose reference summary (handles imprecision correctly)
501                            if let Some(arg_summary) =
502                                slot_reference_summaries.get(&arg_slot).cloned()
503                            {
504                                let composed = compose_return_reference_summary(
505                                    &arg_summary,
506                                    callee_summary,
507                                );
508
509                                // Only compose origin when projection precision is preserved.
510                                // Origin is always-precise (Vec, not Option<Vec>); if projection
511                                // loses precision the origin becomes meaningless.
512                                if composed.projection.is_some() {
513                                    if let Some((_, origin)) =
514                                        slot_reference_origins.get(&arg_slot).cloned()
515                                    {
516                                        // callee_proj is guaranteed Some and Field-free here
517                                        if let Some(ref callee_proj) = callee_summary.projection {
518                                            let mut proj = origin.projection.clone();
519                                            proj.extend(callee_proj.iter().copied());
520                                            slot_reference_origins.insert(
521                                                dest_slot,
522                                                (
523                                                    composed.kind,
524                                                    ReferenceOrigin {
525                                                        root: origin.root,
526                                                        projection: proj,
527                                                    },
528                                                ),
529                                            );
530                                        }
531                                    }
532                                    // Ref params seed summaries but NOT origins (solver.rs:106).
533                                    // If arg has summary but no origin, origin stays cleared.
534                                }
535                                // else: projection lost → origin stays cleared
536
537                                slot_reference_summaries.insert(dest_slot, composed);
538                            }
539                        }
540                    }
541                }
542            }
543        }
544    }
545
546    // Detect potential conflicts between loans on the same place
547    let loan_ids: Vec<u32> = facts.loan_info.keys().copied().collect();
548    for i in 0..loan_ids.len() {
549        for j in (i + 1)..loan_ids.len() {
550            let a = loan_ids[i];
551            let b = loan_ids[j];
552            let info_a = &facts.loan_info[&a];
553            let info_b = &facts.loan_info[&b];
554
555            // Two loans conflict if they borrow overlapping places and at least one is exclusive
556            if info_a.borrowed_place.conflicts_with(&info_b.borrowed_place)
557                && (info_a.kind == BorrowKind::Exclusive || info_b.kind == BorrowKind::Exclusive)
558            {
559                facts.potential_conflicts.push((a, b));
560            }
561        }
562    }
563
564    facts
565}
566
567fn operand_read_places<'a>(operand: &'a Operand, reads: &mut Vec<Place>) {
568    match operand {
569        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
570            reads.push(place.clone());
571            place_nested_read_places(place, reads);
572        }
573        Operand::Constant(_) => {}
574    }
575}
576
577fn place_nested_read_places(place: &Place, reads: &mut Vec<Place>) {
578    match place {
579        Place::Local(_) => {}
580        Place::Field(base, _) | Place::Deref(base) => {
581            place_nested_read_places(base, reads);
582        }
583        Place::Index(base, index) => {
584            place_nested_read_places(base, reads);
585            operand_read_places(index, reads);
586        }
587    }
588}
589
590fn statement_read_places(kind: &StatementKind) -> Vec<Place> {
591    let mut reads = Vec::new();
592    match kind {
593        StatementKind::Assign(_, rvalue) => match rvalue {
594            Rvalue::Use(operand) | Rvalue::Clone(operand) => {
595                operand_read_places(operand, &mut reads)
596            }
597            Rvalue::Borrow(_, _) => {}
598            Rvalue::BinaryOp(_, lhs, rhs) => {
599                operand_read_places(lhs, &mut reads);
600                operand_read_places(rhs, &mut reads);
601            }
602            Rvalue::UnaryOp(_, operand) => operand_read_places(operand, &mut reads),
603            Rvalue::Aggregate(operands) => {
604                for operand in operands {
605                    operand_read_places(operand, &mut reads);
606                }
607            }
608        },
609        StatementKind::Drop(place) => place_nested_read_places(place, &mut reads),
610        StatementKind::TaskBoundary(operands, _kind) => {
611            for operand in operands {
612                operand_read_places(operand, &mut reads);
613            }
614        }
615        StatementKind::ClosureCapture { operands, .. } => {
616            for operand in operands {
617                operand_read_places(operand, &mut reads);
618            }
619        }
620        StatementKind::ArrayStore { operands, .. } => {
621            for operand in operands {
622                operand_read_places(operand, &mut reads);
623            }
624        }
625        StatementKind::ObjectStore { operands, .. } => {
626            for operand in operands {
627                operand_read_places(operand, &mut reads);
628            }
629        }
630        StatementKind::EnumStore { operands, .. } => {
631            for operand in operands {
632                operand_read_places(operand, &mut reads);
633            }
634        }
635        StatementKind::Nop => {}
636    }
637    reads
638}
639
640fn local_loans_from_operand(slot_loans: &HashMap<SlotId, Vec<u32>>, operand: &Operand) -> Vec<u32> {
641    match operand {
642        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => slot_loans
643            .get(&place.root_local())
644            .cloned()
645            .unwrap_or_default(),
646        Operand::Constant(_) => Vec::new(),
647    }
648}
649
650fn local_loans_from_operands(
651    slot_loans: &HashMap<SlotId, Vec<u32>>,
652    operands: &[Operand],
653) -> Vec<u32> {
654    let mut loans = Vec::new();
655    let mut seen = HashSet::new();
656    for operand in operands {
657        for loan in local_loans_from_operand(slot_loans, operand) {
658            if seen.insert(loan) {
659                loans.push(loan);
660            }
661        }
662    }
663    loans
664}
665
666fn update_slot_loan_aliases(
667    slot_loans: &mut HashMap<SlotId, Vec<u32>>,
668    dest_slot: SlotId,
669    rvalue: &Rvalue,
670) {
671    match rvalue {
672        Rvalue::Borrow(_, _) => {}
673        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
674        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
675        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
676        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
677        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
678            if let Some(loans) = slot_loans.get(src_slot).cloned() {
679                slot_loans.insert(dest_slot, loans);
680            } else {
681                slot_loans.remove(&dest_slot);
682            }
683        }
684        _ => {
685            slot_loans.remove(&dest_slot);
686        }
687    }
688}
689
690fn local_loans_from_rvalue(slot_loans: &HashMap<SlotId, Vec<u32>>, rvalue: &Rvalue) -> Vec<u32> {
691    match rvalue {
692        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
693        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
694        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
695        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
696        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
697            slot_loans.get(src_slot).cloned().unwrap_or_default()
698        }
699        _ => Vec::new(),
700    }
701}
702
703fn update_slot_reference_summaries(
704    slot_reference_summaries: &mut HashMap<SlotId, ReturnReferenceSummary>,
705    dest_slot: SlotId,
706    rvalue: &Rvalue,
707) {
708    match rvalue {
709        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
710        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
711        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
712        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
713        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
714            if let Some(contract) = slot_reference_summaries.get(src_slot).cloned() {
715                slot_reference_summaries.insert(dest_slot, contract);
716            } else {
717                slot_reference_summaries.remove(&dest_slot);
718            }
719        }
720        _ => {
721            slot_reference_summaries.remove(&dest_slot);
722        }
723    }
724}
725
726fn reference_summary_from_rvalue(
727    slot_reference_summaries: &HashMap<SlotId, ReturnReferenceSummary>,
728    rvalue: &Rvalue,
729) -> Option<ReturnReferenceSummary> {
730    match rvalue {
731        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
732        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
733        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
734        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
735        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
736            slot_reference_summaries.get(src_slot).cloned()
737        }
738        _ => None,
739    }
740}
741
742fn update_slot_reference_origins(
743    slot_reference_origins: &mut HashMap<SlotId, (BorrowKind, ReferenceOrigin)>,
744    dest_slot: SlotId,
745    rvalue: &Rvalue,
746) {
747    match rvalue {
748        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
749        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
750        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
751        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
752        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
753            if let Some(origin) = slot_reference_origins.get(src_slot).cloned() {
754                slot_reference_origins.insert(dest_slot, origin);
755            } else {
756                slot_reference_origins.remove(&dest_slot);
757            }
758        }
759        _ => {
760            slot_reference_origins.remove(&dest_slot);
761        }
762    }
763}
764
765fn reference_origin_from_rvalue(
766    slot_reference_origins: &HashMap<SlotId, (BorrowKind, ReferenceOrigin)>,
767    rvalue: &Rvalue,
768) -> Option<(BorrowKind, ReferenceOrigin)> {
769    match rvalue {
770        Rvalue::Borrow(kind, place) => Some((
771            *kind,
772            reference_origin_for_place(place, &[]),
773        )),
774        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
775        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
776        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
777        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
778        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
779            slot_reference_origins.get(src_slot).cloned()
780        }
781        _ => None,
782    }
783}
784
785fn reference_origin_for_place(place: &Place, param_slots: &[SlotId]) -> ReferenceOrigin {
786    let root_slot = place.root_local();
787    let root = param_slots
788        .iter()
789        .position(|slot| *slot == root_slot)
790        .map(ReferenceOriginRoot::Param)
791        .unwrap_or(ReferenceOriginRoot::Local(root_slot));
792    ReferenceOrigin {
793        root,
794        projection: place.projection_steps(),
795    }
796}
797
798fn reference_summary_from_origin(
799    borrow_kind: BorrowKind,
800    origin: &ReferenceOrigin,
801) -> Option<ReturnReferenceSummary> {
802    match origin.root {
803        ReferenceOriginRoot::Param(param_index) => Some(ReturnReferenceSummary {
804            param_index,
805            kind: borrow_kind,
806            projection: Some(origin.projection.clone()),
807        }),
808        ReferenceOriginRoot::Local(_) => None,
809    }
810}
811
812fn safe_reference_summary_for_borrow(
813    borrow_kind: BorrowKind,
814    borrowed_place: &Place,
815    param_reference_summaries: &HashMap<SlotId, ReturnReferenceSummary>,
816) -> Option<ReturnReferenceSummary> {
817    // Support both direct param borrows (&param) and field-of-param borrows (&param.field).
818    // The root local must be a parameter with a reference summary.
819    let param_summary = param_reference_summaries.get(&borrowed_place.root_local())?;
820    Some(ReturnReferenceSummary {
821        param_index: param_summary.param_index,
822        kind: borrow_kind,
823        projection: Some(borrowed_place.projection_steps()),
824    })
825}
826
827/// Compose a callee's return summary with the argument slot's existing summary.
828///
829/// - `param_index`: from `arg_summary` (traces to the caller's parameter)
830/// - `kind`: from `callee_summary` (callee dictates the returned borrow kind)
831/// - `projection`: concatenate only when BOTH are `Some` AND the callee
832///   projection contains no `Field` steps (FieldIdx is per-MirBuilder,
833///   not cross-function stable). Otherwise `None` (precision lost).
834fn compose_return_reference_summary(
835    arg_summary: &ReturnReferenceSummary,
836    callee_summary: &ReturnReferenceSummary,
837) -> ReturnReferenceSummary {
838    let projection = match (&arg_summary.projection, &callee_summary.projection) {
839        (Some(arg_proj), Some(callee_proj)) => {
840            if callee_proj
841                .iter()
842                .any(|step| matches!(step, ProjectionStep::Field(_)))
843            {
844                None // FieldIdx is per-MirBuilder, unsound across functions
845            } else {
846                let mut composed = arg_proj.clone();
847                composed.extend(callee_proj.iter().copied());
848                Some(composed)
849            }
850        }
851        _ => None, // precision already lost on one side
852    };
853    ReturnReferenceSummary {
854        param_index: arg_summary.param_index,
855        kind: callee_summary.kind,
856        projection,
857    }
858}
859
860fn resolve_return_reference_summary(
861    errors: &mut Vec<BorrowError>,
862    facts: &BorrowFacts,
863    loans_at_point: &HashMap<Point, Vec<LoanId>>,
864) -> Option<ReturnReferenceSummary> {
865    let mut merged_candidate: Option<ReturnReferenceSummary> = None;
866    let mut inconsistent = false;
867    for (candidate, _) in &facts.return_reference_candidates {
868        if let Some(existing) = merged_candidate.as_mut() {
869            if existing.param_index != candidate.param_index || existing.kind != candidate.kind {
870                inconsistent = true;
871                break;
872            }
873            if existing.projection != candidate.projection {
874                existing.projection = None;
875            }
876        } else {
877            merged_candidate = Some(candidate.clone());
878        }
879    }
880
881    if merged_candidate.is_none() {
882        return None;
883    }
884
885    let error_span = if inconsistent {
886        facts
887            .return_reference_candidates
888            .get(1)
889            .map(|(_, span)| *span)
890    } else {
891        facts.non_reference_return_spans.first().copied()
892    };
893
894    if let Some(span) = error_span {
895        let (conflicting_loan, loan_span, last_use_span) = facts
896            .return_reference_candidates
897            .first()
898            .and_then(|(candidate, candidate_span)| {
899                find_matching_loan_for_return_candidate(
900                    candidate,
901                    *candidate_span,
902                    facts,
903                    loans_at_point,
904                )
905            })
906            .unwrap_or((LoanId(0), span, None));
907        errors.push(BorrowError {
908            kind: BorrowErrorKind::InconsistentReferenceReturn,
909            span,
910            conflicting_loan,
911            loan_span,
912            last_use_span,
913            repairs: Vec::new(),
914        });
915        return None;
916    }
917
918    merged_candidate
919}
920
921fn find_matching_loan_for_return_candidate(
922    candidate: &ReturnReferenceSummary,
923    candidate_span: shape_ast::ast::Span,
924    facts: &BorrowFacts,
925    loans_at_point: &HashMap<Point, Vec<LoanId>>,
926) -> Option<(LoanId, shape_ast::ast::Span, Option<shape_ast::ast::Span>)> {
927    let point = facts
928        .point_spans
929        .iter()
930        .find_map(|(point, span)| (*span == candidate_span).then_some(Point(*point)))?;
931    let loans = loans_at_point.get(&point)?;
932    for loan in loans {
933        let info = facts.loan_info.get(&loan.0)?;
934        if info.kind == candidate.kind {
935            return Some((*loan, info.span, last_use_span_for_loan(facts, loan.0)));
936        }
937    }
938    None
939}
940
941/// Run the Datafrog solver to compute loan liveness and detect errors.
942pub fn solve(facts: &BorrowFacts) -> SolverResult {
943    let mut iteration = Iteration::new();
944
945    // Input relations (static — known before iteration)
946    // cfg_edge indexed by source point: (point1, point2)
947    let cfg_edge: Relation<(u32, u32)> = facts.cfg_edge.iter().cloned().collect();
948    // invalidates indexed by (point, loan)
949    let invalidates_set: std::collections::HashSet<(u32, u32)> =
950        facts.invalidates.iter().cloned().collect();
951
952    // Derived relation: loan_live_at(point, loan)
953    // Keyed by point for efficient join with cfg_edge.
954    let loan_live_at = iteration.variable::<(u32, u32)>("loan_live_at");
955
956    // Seed: a loan is live at the point where it's issued.
957    // Reindex from (loan, point) to (point, loan).
958    let seed: Vec<(u32, u32)> = facts
959        .loan_issued_at
960        .iter()
961        .map(|&(loan, point)| (point, loan))
962        .collect();
963    loan_live_at.extend(seed.iter().cloned());
964
965    // Fixed-point iteration:
966    // loan_live_at(point2, loan) :-
967    //   loan_live_at(point1, loan),
968    //   cfg_edge(point1, point2),
969    //   !invalidates(point1, loan).
970    while iteration.changed() {
971        // For each (point1, loan) in loan_live_at,
972        // join with cfg_edge on point1 to get point2,
973        // filter out if invalidates(point1, loan).
974        loan_live_at.from_leapjoin(
975            &loan_live_at,
976            cfg_edge.extend_with(|&(point1, _loan)| point1),
977            |&(point1, loan), &point2| {
978                if invalidates_set.contains(&(point1, loan)) {
979                    // Loan is invalidated at point1 — keep it live at point1,
980                    // but don't propagate it to successors.
981                    (u32::MAX, u32::MAX) // sentinel that won't match anything useful
982                } else {
983                    (point2, loan)
984                }
985            },
986        );
987    }
988
989    // Collect results and filter out sentinel values
990    let forward_live_points: Vec<(u32, u32)> = loan_live_at
991        .complete()
992        .iter()
993        .filter(|&&(p, l)| p != u32::MAX && l != u32::MAX)
994        .cloned()
995        .collect();
996    let (nll_live_set, loans_with_reachable_uses) = compute_nll_live_points(facts);
997    let loan_live_at_result: Vec<(u32, u32)> = forward_live_points
998        .into_iter()
999        .filter(|point_loan| {
1000            !loans_with_reachable_uses.contains(&point_loan.1) || nll_live_set.contains(point_loan)
1001        })
1002        .collect();
1003
1004    // Build point → active loans map
1005    let mut loans_at_point: HashMap<Point, Vec<LoanId>> = HashMap::new();
1006    for &(point, loan) in &loan_live_at_result {
1007        loans_at_point
1008            .entry(Point(point))
1009            .or_default()
1010            .push(LoanId(loan));
1011    }
1012
1013    // Build loan → set of points for quick intersection queries
1014    let mut loan_points: HashMap<u32, std::collections::HashSet<u32>> = HashMap::new();
1015    for &(point, loan) in &loan_live_at_result {
1016        loan_points.entry(loan).or_default().insert(point);
1017    }
1018
1019    // Detect errors: two conflicting loans alive at the same point
1020    let mut errors = Vec::new();
1021    let mut seen_conflicts = std::collections::HashSet::new();
1022    for &(loan_a, loan_b) in &facts.potential_conflicts {
1023        let key = (loan_a.min(loan_b), loan_a.max(loan_b));
1024        if !seen_conflicts.insert(key) {
1025            continue;
1026        }
1027
1028        let points_a = loan_points.get(&loan_a);
1029        let points_b = loan_points.get(&loan_b);
1030
1031        if let (Some(pa), Some(pb)) = (points_a, points_b) {
1032            // Check if there's any intersection
1033            let has_overlap = pa.iter().any(|p| pb.contains(p));
1034            if has_overlap {
1035                let info_a = &facts.loan_info[&loan_a];
1036                let info_b = &facts.loan_info[&loan_b];
1037                let kind = if info_a.kind == BorrowKind::Exclusive
1038                    && info_b.kind == BorrowKind::Exclusive
1039                {
1040                    BorrowErrorKind::ConflictExclusiveExclusive
1041                } else {
1042                    BorrowErrorKind::ConflictSharedExclusive
1043                };
1044                errors.push(BorrowError {
1045                    kind,
1046                    span: info_b.span,
1047                    conflicting_loan: LoanId(loan_a),
1048                    loan_span: info_a.span,
1049                    last_use_span: last_use_span_for_loan(facts, loan_a),
1050                    repairs: Vec::new(),
1051                });
1052            }
1053        }
1054    }
1055
1056    let mut seen_writes = std::collections::HashSet::new();
1057    for (point, place, span) in &facts.writes {
1058        let point_key = Point(*point);
1059        let Some(loans) = loans_at_point.get(&point_key) else {
1060            continue;
1061        };
1062        for loan in loans {
1063            let info = &facts.loan_info[&loan.0];
1064            if !place.conflicts_with(&info.borrowed_place) {
1065                continue;
1066            }
1067            let key = (*point, loan.0);
1068            if !seen_writes.insert(key) {
1069                continue;
1070            }
1071            errors.push(BorrowError {
1072                kind: BorrowErrorKind::WriteWhileBorrowed,
1073                span: *span,
1074                conflicting_loan: *loan,
1075                loan_span: info.span,
1076                last_use_span: last_use_span_for_loan(facts, loan.0),
1077                repairs: Vec::new(),
1078            });
1079            break;
1080        }
1081    }
1082
1083    let mut seen_reads = std::collections::HashSet::new();
1084    for (point, place, span) in &facts.reads {
1085        let point_key = Point(*point);
1086        let Some(loans) = loans_at_point.get(&point_key) else {
1087            continue;
1088        };
1089        for loan in loans {
1090            let info = &facts.loan_info[&loan.0];
1091            if info.kind != BorrowKind::Exclusive || !place.conflicts_with(&info.borrowed_place) {
1092                continue;
1093            }
1094            let key = (*point, loan.0);
1095            if !seen_reads.insert(key) {
1096                continue;
1097            }
1098            errors.push(BorrowError {
1099                kind: BorrowErrorKind::ReadWhileExclusivelyBorrowed,
1100                span: *span,
1101                conflicting_loan: *loan,
1102                loan_span: info.span,
1103                last_use_span: last_use_span_for_loan(facts, loan.0),
1104                repairs: Vec::new(),
1105            });
1106            break;
1107        }
1108    }
1109
1110    let mut seen_escapes = std::collections::HashSet::new();
1111    for (loan_id, span) in &facts.escaped_loans {
1112        if !seen_escapes.insert((*loan_id, span.start, span.end)) {
1113            continue;
1114        }
1115        let info = &facts.loan_info[loan_id];
1116        errors.push(BorrowError {
1117            kind: BorrowErrorKind::ReferenceEscape,
1118            span: *span,
1119            conflicting_loan: LoanId(*loan_id),
1120            loan_span: info.span,
1121            last_use_span: last_use_span_for_loan(facts, *loan_id),
1122            repairs: Vec::new(),
1123        });
1124    }
1125
1126    let mut seen_sinks = std::collections::HashSet::new();
1127    for sink in &facts.loan_sinks {
1128        let key = (
1129            sink.loan_id,
1130            sink.kind,
1131            sink.span.start,
1132            sink.span.end,
1133            sink.sink_slot.map(|slot| slot.0),
1134        );
1135        if !seen_sinks.insert(key) {
1136            continue;
1137        }
1138
1139        let info = &facts.loan_info[&sink.loan_id];
1140        let sink_is_local = sink
1141            .sink_slot
1142            .and_then(|slot| facts.slot_escape_status.get(&slot).copied())
1143            == Some(EscapeStatus::Local);
1144
1145        let kind = match sink.kind {
1146            LoanSinkKind::ReturnSlot => continue,
1147            LoanSinkKind::ClosureEnv if sink_is_local => continue,
1148            LoanSinkKind::ClosureEnv => BorrowErrorKind::ReferenceEscapeIntoClosure,
1149            LoanSinkKind::ArrayStore | LoanSinkKind::ArrayAssignment if sink_is_local => continue,
1150            LoanSinkKind::ArrayStore | LoanSinkKind::ArrayAssignment => {
1151                BorrowErrorKind::ReferenceStoredInArray
1152            }
1153            LoanSinkKind::ObjectStore | LoanSinkKind::ObjectAssignment if sink_is_local => continue,
1154            LoanSinkKind::ObjectStore | LoanSinkKind::ObjectAssignment => {
1155                BorrowErrorKind::ReferenceStoredInObject
1156            }
1157            LoanSinkKind::EnumStore if sink_is_local => continue,
1158            LoanSinkKind::EnumStore => BorrowErrorKind::ReferenceStoredInEnum,
1159            LoanSinkKind::StructuredTaskBoundary => {
1160                BorrowErrorKind::ExclusiveRefAcrossTaskBoundary
1161            }
1162            LoanSinkKind::DetachedTaskBoundary if info.kind == BorrowKind::Exclusive => {
1163                BorrowErrorKind::ExclusiveRefAcrossTaskBoundary
1164            }
1165            LoanSinkKind::DetachedTaskBoundary => BorrowErrorKind::SharedRefAcrossDetachedTask,
1166        };
1167
1168        errors.push(BorrowError {
1169            kind,
1170            span: sink.span,
1171            conflicting_loan: LoanId(sink.loan_id),
1172            loan_span: info.span,
1173            last_use_span: last_use_span_for_loan(facts, sink.loan_id),
1174            repairs: Vec::new(),
1175        });
1176    }
1177
1178    // Non-sendable values across detached task boundaries
1179    let mut seen_non_sendable = std::collections::HashSet::new();
1180    for (slot_id, span) in &facts.non_sendable_task_boundary {
1181        if !seen_non_sendable.insert((*slot_id, span.start, span.end)) {
1182            continue;
1183        }
1184        errors.push(BorrowError {
1185            kind: BorrowErrorKind::NonSendableAcrossTaskBoundary,
1186            span: *span,
1187            conflicting_loan: LoanId(0),
1188            loan_span: *span,
1189            last_use_span: None,
1190            repairs: Vec::new(),
1191        });
1192    }
1193
1194    let return_reference_summary =
1195        resolve_return_reference_summary(&mut errors, facts, &loans_at_point);
1196
1197    SolverResult {
1198        loans_at_point,
1199        errors,
1200        loan_info: facts.loan_info.clone(),
1201        return_reference_summary,
1202    }
1203}
1204
1205fn compute_nll_live_points(facts: &BorrowFacts) -> (HashSet<(u32, u32)>, HashSet<u32>) {
1206    let mut predecessors: HashMap<u32, Vec<u32>> = HashMap::new();
1207    for (from, to) in &facts.cfg_edge {
1208        predecessors.entry(*to).or_default().push(*from);
1209    }
1210
1211    let issue_points: HashMap<u32, u32> = facts
1212        .loan_issued_at
1213        .iter()
1214        .map(|(loan_id, point)| (*loan_id, *point))
1215        .collect();
1216
1217    let mut invalidation_points: HashMap<u32, HashSet<u32>> = HashMap::new();
1218    for (point, loan_id) in &facts.invalidates {
1219        invalidation_points
1220            .entry(*loan_id)
1221            .or_default()
1222            .insert(*point);
1223    }
1224
1225    let mut use_points: HashMap<u32, Vec<u32>> = HashMap::new();
1226    for (loan_id, point) in &facts.use_of_loan {
1227        use_points.entry(*loan_id).or_default().push(*point);
1228    }
1229
1230    let mut live_points = HashSet::new();
1231    let mut loans_with_reachable_uses = HashSet::new();
1232    for (loan_id, issue_point) in issue_points {
1233        let mut worklist = use_points.get(&loan_id).cloned().unwrap_or_default();
1234        let invalidates = invalidation_points.get(&loan_id);
1235        let mut visited = HashSet::new();
1236        let mut loan_live_points = HashSet::new();
1237        let mut reached_issue = false;
1238
1239        while let Some(point) = worklist.pop() {
1240            if !visited.insert(point) {
1241                continue;
1242            }
1243
1244            loan_live_points.insert((point, loan_id));
1245
1246            if point == issue_point {
1247                reached_issue = true;
1248                continue;
1249            }
1250
1251            if invalidates.is_some_and(|points| points.contains(&point)) {
1252                continue;
1253            }
1254
1255            if let Some(preds) = predecessors.get(&point) {
1256                worklist.extend(preds.iter().copied());
1257            }
1258        }
1259
1260        if reached_issue {
1261            loans_with_reachable_uses.insert(loan_id);
1262            live_points.extend(loan_live_points);
1263        }
1264    }
1265
1266    (live_points, loans_with_reachable_uses)
1267}
1268
1269/// Raw solver output (before combining with liveness for full BorrowAnalysis).
1270#[derive(Debug)]
1271pub struct SolverResult {
1272    pub loans_at_point: HashMap<Point, Vec<LoanId>>,
1273    pub errors: Vec<BorrowError>,
1274    pub loan_info: HashMap<u32, LoanInfo>,
1275    pub return_reference_summary: Option<ReturnReferenceSummary>,
1276}
1277
1278/// Run the complete borrow analysis pipeline for a MIR function.
1279/// This is the main entry point — produces the single BorrowAnalysis
1280/// consumed by compiler, LSP, and diagnostics.
1281/// Extract a borrow summary for a function — describes which parameters are
1282/// borrowed and which parameter pairs must not alias at call sites.
1283pub fn extract_borrow_summary(
1284    mir: &MirFunction,
1285    return_summary: Option<ReturnReferenceSummary>,
1286) -> FunctionBorrowSummary {
1287    let num_params = mir.param_slots.len();
1288    let mut param_borrows: Vec<Option<BorrowKind>> = mir
1289        .param_reference_kinds
1290        .iter()
1291        .cloned()
1292        .collect();
1293    // Pad to num_params if param_reference_kinds is shorter
1294    while param_borrows.len() < num_params {
1295        param_borrows.push(None);
1296    }
1297
1298    // Determine which params are written to (mutated) in the function body
1299    let mut mutated_params: HashSet<usize> = HashSet::new();
1300    let mut read_params: HashSet<usize> = HashSet::new();
1301    for block in mir.iter_blocks() {
1302        for stmt in &block.statements {
1303            match &stmt.kind {
1304                StatementKind::Assign(dest, rvalue) => {
1305                    // Check if dest's root is a parameter (handles Local, Field, Index)
1306                    let root = dest.root_local();
1307                    if let Some(param_idx) = mir.param_slots.iter().position(|s| *s == root) {
1308                        mutated_params.insert(param_idx);
1309                    }
1310                    // Check if any param is read in the rvalue
1311                    for param_idx in 0..num_params {
1312                        if rvalue_uses_param(rvalue, mir.param_slots[param_idx]) {
1313                            read_params.insert(param_idx);
1314                        }
1315                    }
1316                }
1317                _ => {}
1318            }
1319        }
1320        // Check terminator args for reads
1321        if let TerminatorKind::Call { args, .. } = &block.terminator.kind {
1322            for arg in args {
1323                for param_idx in 0..num_params {
1324                    if operand_uses_param(arg, mir.param_slots[param_idx]) {
1325                        read_params.insert(param_idx);
1326                    }
1327                }
1328            }
1329        }
1330    }
1331
1332    // Compute effective borrow kind per param: explicit annotations take priority,
1333    // otherwise infer from usage — mutated → Exclusive, read → Shared.
1334    let mut effective_borrows: Vec<Option<BorrowKind>> = param_borrows.clone();
1335    for idx in 0..num_params {
1336        if effective_borrows[idx].is_none() {
1337            if mutated_params.contains(&idx) {
1338                effective_borrows[idx] = Some(BorrowKind::Exclusive);
1339            } else if read_params.contains(&idx) {
1340                effective_borrows[idx] = Some(BorrowKind::Shared);
1341            }
1342        }
1343    }
1344
1345    // Build conflict pairs: a mutated param conflicts with every other param
1346    // that is read or borrowed (shared or exclusive).
1347    let mut conflict_pairs = Vec::new();
1348    for &mutated_idx in &mutated_params {
1349        for other_idx in 0..num_params {
1350            if other_idx == mutated_idx {
1351                continue;
1352            }
1353            // Mutated param conflicts with any other param that is used
1354            if effective_borrows[other_idx].is_some() {
1355                conflict_pairs.push((mutated_idx, other_idx));
1356            }
1357        }
1358    }
1359    // Also: two exclusive borrows on different params always conflict
1360    for i in 0..num_params {
1361        for j in (i + 1)..num_params {
1362            if effective_borrows[i] == Some(BorrowKind::Exclusive)
1363                && effective_borrows[j] == Some(BorrowKind::Exclusive)
1364                && !conflict_pairs.contains(&(i, j))
1365                && !conflict_pairs.contains(&(j, i))
1366            {
1367                conflict_pairs.push((i, j));
1368            }
1369        }
1370    }
1371
1372    FunctionBorrowSummary {
1373        param_borrows,
1374        conflict_pairs,
1375        return_summary,
1376    }
1377}
1378
1379fn rvalue_uses_param(rvalue: &Rvalue, param_slot: SlotId) -> bool {
1380    match rvalue {
1381        Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => {
1382            operand_uses_param(op, param_slot)
1383        }
1384        Rvalue::Borrow(_, place) => place.root_local() == param_slot,
1385        Rvalue::BinaryOp(_, lhs, rhs) => {
1386            operand_uses_param(lhs, param_slot) || operand_uses_param(rhs, param_slot)
1387        }
1388        Rvalue::Aggregate(ops) => ops.iter().any(|op| operand_uses_param(op, param_slot)),
1389    }
1390}
1391
1392fn operand_uses_param(op: &Operand, param_slot: SlotId) -> bool {
1393    match op {
1394        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
1395            place.root_local() == param_slot
1396        }
1397        Operand::Constant(_) => false,
1398    }
1399}
1400
1401pub fn analyze(mir: &MirFunction, callee_summaries: &CalleeSummaries) -> BorrowAnalysis {
1402    let cfg = ControlFlowGraph::build(mir);
1403
1404    // 1. Compute liveness (for move/clone inference)
1405    let liveness = liveness::compute_liveness(mir, &cfg);
1406
1407    // 2. Extract Datafrog input facts
1408    let facts = extract_facts(mir, &cfg, callee_summaries);
1409
1410    // 3. Run the Datafrog solver
1411    let solver_result = solve(&facts);
1412
1413    // 4. Compute ownership decisions (move/clone) based on liveness
1414    let ownership_decisions = compute_ownership_decisions(mir, &liveness);
1415    let mut move_errors = compute_use_after_move_errors(mir, &cfg, &ownership_decisions);
1416
1417    // 5. Combine into BorrowAnalysis
1418    let loans = solver_result
1419        .loan_info
1420        .into_iter()
1421        .map(|(id, info)| (LoanId(id), info))
1422        .collect();
1423    let mut errors = solver_result.errors;
1424    errors.append(&mut move_errors);
1425
1426    BorrowAnalysis {
1427        liveness,
1428        loans_at_point: solver_result.loans_at_point,
1429        loans,
1430        errors,
1431        ownership_decisions,
1432        mutability_errors: Vec::new(), // filled by binding resolver (Phase 1)
1433        return_reference_summary: solver_result.return_reference_summary,
1434    }
1435}
1436
1437/// Compute ownership decisions for assignments based on liveness.
1438fn compute_ownership_decisions(
1439    mir: &MirFunction,
1440    liveness: &LivenessResult,
1441) -> HashMap<Point, OwnershipDecision> {
1442    let mut decisions = HashMap::new();
1443
1444    for block in &mir.blocks {
1445        for (stmt_idx, stmt) in block.statements.iter().enumerate() {
1446            if let StatementKind::Assign(_, Rvalue::Use(Operand::Move(Place::Local(src_slot)))) =
1447                &stmt.kind
1448            {
1449                // Check if the source is a non-Copy type
1450                let src_type = mir
1451                    .local_types
1452                    .get(src_slot.0 as usize)
1453                    .cloned()
1454                    .unwrap_or(LocalTypeInfo::Unknown);
1455
1456                let decision = match src_type {
1457                    LocalTypeInfo::Copy => OwnershipDecision::Copy,
1458                    LocalTypeInfo::NonCopy => {
1459                        // Smart inference: check if source is live after this point
1460                        if liveness.is_live_after(block.id, stmt_idx, *src_slot, mir) {
1461                            OwnershipDecision::Clone
1462                        } else {
1463                            OwnershipDecision::Move
1464                        }
1465                    }
1466                    LocalTypeInfo::Unknown => {
1467                        // Conservative: assume Clone if live, Move if dead
1468                        if liveness.is_live_after(block.id, stmt_idx, *src_slot, mir) {
1469                            OwnershipDecision::Clone
1470                        } else {
1471                            OwnershipDecision::Move
1472                        }
1473                    }
1474                };
1475
1476                decisions.insert(stmt.point, decision);
1477            }
1478        }
1479    }
1480
1481    decisions
1482}
1483
1484fn compute_use_after_move_errors(
1485    mir: &MirFunction,
1486    cfg: &ControlFlowGraph,
1487    ownership_decisions: &HashMap<Point, OwnershipDecision>,
1488) -> Vec<BorrowError> {
1489    let mut in_states: HashMap<BasicBlockId, HashMap<Place, shape_ast::ast::Span>> = HashMap::new();
1490    let mut out_states: HashMap<BasicBlockId, HashMap<Place, shape_ast::ast::Span>> =
1491        HashMap::new();
1492
1493    for block in mir.iter_blocks() {
1494        in_states.insert(block.id, HashMap::new());
1495        out_states.insert(block.id, HashMap::new());
1496    }
1497
1498    let mut changed = true;
1499    while changed {
1500        changed = false;
1501        for &block_id in &cfg.reverse_postorder() {
1502            let mut block_in: Option<HashMap<Place, shape_ast::ast::Span>> = None;
1503            for &pred in cfg.predecessors(block_id) {
1504                if let Some(pred_out) = out_states.get(&pred) {
1505                    if let Some(current) = block_in.as_mut() {
1506                        intersect_moved_places(current, pred_out);
1507                    } else {
1508                        block_in = Some(pred_out.clone());
1509                    }
1510                }
1511            }
1512            let block_in = block_in.unwrap_or_default();
1513
1514            let mut block_out = block_in.clone();
1515            let block = mir.block(block_id);
1516            for stmt in &block.statements {
1517                apply_move_transfer(&mut block_out, stmt, mir, ownership_decisions);
1518            }
1519            // Also apply Call terminator moves (destination write clears moved status)
1520            apply_terminator_move_transfer(&mut block_out, &block.terminator);
1521
1522            if in_states.get(&block_id) != Some(&block_in) {
1523                in_states.insert(block_id, block_in);
1524                changed = true;
1525            }
1526            if out_states.get(&block_id) != Some(&block_out) {
1527                out_states.insert(block_id, block_out);
1528                changed = true;
1529            }
1530        }
1531    }
1532
1533    let mut errors = Vec::new();
1534    let mut seen = HashSet::new();
1535    for block in mir.iter_blocks() {
1536        let mut moved_places = in_states.get(&block.id).cloned().unwrap_or_default();
1537        for stmt in &block.statements {
1538            for read_place in statement_read_places(&stmt.kind) {
1539                if let Some((moved_place, move_span)) =
1540                    find_moved_place_conflict(&moved_places, &read_place)
1541                {
1542                    let key = (stmt.point.0, format!("{}", moved_place));
1543                    if seen.insert(key) {
1544                        errors.push(BorrowError {
1545                            kind: BorrowErrorKind::UseAfterMove,
1546                            span: stmt.span,
1547                            conflicting_loan: LoanId(0),
1548                            loan_span: move_span,
1549                            last_use_span: None,
1550                            repairs: Vec::new(),
1551                        });
1552                    }
1553                    break;
1554                }
1555            }
1556
1557            if let Some(borrowed_place) = statement_borrow_place(&stmt.kind)
1558                && let Some((moved_place, move_span)) =
1559                    find_moved_place_conflict(&moved_places, borrowed_place)
1560            {
1561                let key = (stmt.point.0, format!("{}", moved_place));
1562                if seen.insert(key) {
1563                    errors.push(BorrowError {
1564                        kind: BorrowErrorKind::UseAfterMove,
1565                        span: stmt.span,
1566                        conflicting_loan: LoanId(0),
1567                        loan_span: move_span,
1568                        last_use_span: None,
1569                        repairs: Vec::new(),
1570                    });
1571                }
1572            }
1573
1574            if let Some(dest_place) = statement_dest_place(&stmt.kind)
1575                && let Some((moved_place, move_span)) = moved_places
1576                    .iter()
1577                    .find(|(moved_place, _)| {
1578                        dest_place.conflicts_with(moved_place)
1579                            && !reinitializes_moved_place(dest_place, moved_place)
1580                    })
1581                    .map(|(place, span)| (place.clone(), *span))
1582            {
1583                let key = (stmt.point.0, format!("{}", moved_place));
1584                if seen.insert(key) {
1585                    errors.push(BorrowError {
1586                        kind: BorrowErrorKind::UseAfterMove,
1587                        span: stmt.span,
1588                        conflicting_loan: LoanId(0),
1589                        loan_span: move_span,
1590                        last_use_span: None,
1591                        repairs: Vec::new(),
1592                    });
1593                }
1594            }
1595
1596            apply_move_transfer(&mut moved_places, stmt, mir, ownership_decisions);
1597        }
1598
1599        // Check Call terminator for reads of moved places, then apply its transfer
1600        if let TerminatorKind::Call { func, args, destination, .. } = &block.terminator.kind {
1601            let term_key_point = block.terminator.span.start as u32;
1602            // Check func operand
1603            if let Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) = func {
1604                if let Some((moved_place, move_span)) = find_moved_place_conflict(&moved_places, place) {
1605                    let key = (term_key_point, format!("{}", moved_place));
1606                    if seen.insert(key) {
1607                        errors.push(BorrowError {
1608                            kind: BorrowErrorKind::UseAfterMove,
1609                            span: block.terminator.span,
1610                            conflicting_loan: LoanId(0),
1611                            loan_span: move_span,
1612                            last_use_span: None,
1613                            repairs: Vec::new(),
1614                        });
1615                    }
1616                }
1617            }
1618            // Check each arg
1619            for arg in args {
1620                if let Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) = arg {
1621                    if let Some((moved_place, move_span)) = find_moved_place_conflict(&moved_places, place) {
1622                        let key = (term_key_point, format!("{}", moved_place));
1623                        if seen.insert(key) {
1624                            errors.push(BorrowError {
1625                                kind: BorrowErrorKind::UseAfterMove,
1626                                span: block.terminator.span,
1627                                conflicting_loan: LoanId(0),
1628                                loan_span: move_span,
1629                                last_use_span: None,
1630                                repairs: Vec::new(),
1631                            });
1632                        }
1633                    }
1634                }
1635            }
1636            // Destination write clears moved status
1637            moved_places.retain(|moved_place, _| !reinitializes_moved_place(destination, moved_place));
1638        }
1639    }
1640
1641    errors
1642}
1643
1644fn intersect_moved_places(
1645    dest: &mut HashMap<Place, shape_ast::ast::Span>,
1646    incoming: &HashMap<Place, shape_ast::ast::Span>,
1647) {
1648    dest.retain(|place, span| {
1649        if let Some(incoming_span) = incoming.get(place) {
1650            if incoming_span.start < span.start {
1651                *span = *incoming_span;
1652            }
1653            true
1654        } else {
1655            false
1656        }
1657    });
1658}
1659
1660fn apply_move_transfer(
1661    moved_places: &mut HashMap<Place, shape_ast::ast::Span>,
1662    stmt: &MirStatement,
1663    mir: &MirFunction,
1664    ownership_decisions: &HashMap<Point, OwnershipDecision>,
1665) {
1666    if let Some(dest_place) = statement_dest_place(&stmt.kind) {
1667        moved_places.retain(|moved_place, _| !reinitializes_moved_place(dest_place, moved_place));
1668    }
1669
1670    for moved_place in actual_move_places(stmt, mir, ownership_decisions) {
1671        moved_places.insert(moved_place, stmt.span);
1672    }
1673}
1674
1675/// Apply move transfer for a Call terminator.
1676/// The call writes its return value to `destination`, which reinitializes that place.
1677/// Call args are typically temp slots created by `lower_expr_as_moved_operand` —
1678/// the moves of source values INTO those temps happen in prior statements (via Assign/Move),
1679/// not in the terminator itself, so we don't need to mark args as moved here.
1680fn apply_terminator_move_transfer(
1681    moved_places: &mut HashMap<Place, shape_ast::ast::Span>,
1682    terminator: &Terminator,
1683) {
1684    if let TerminatorKind::Call { destination, .. } = &terminator.kind {
1685        // The call writes to destination, which reinitializes that place
1686        moved_places.retain(|moved_place, _| !reinitializes_moved_place(destination, moved_place));
1687    }
1688}
1689
1690fn statement_borrow_place(kind: &StatementKind) -> Option<&Place> {
1691    match kind {
1692        StatementKind::Assign(_, Rvalue::Borrow(_, place)) => Some(place),
1693        _ => None,
1694    }
1695}
1696
1697fn statement_dest_place(kind: &StatementKind) -> Option<&Place> {
1698    match kind {
1699        StatementKind::Assign(place, _) | StatementKind::Drop(place) => Some(place),
1700        StatementKind::TaskBoundary(..)
1701        | StatementKind::ClosureCapture { .. }
1702        | StatementKind::ArrayStore { .. }
1703        | StatementKind::ObjectStore { .. }
1704        | StatementKind::EnumStore { .. } => None,
1705        StatementKind::Nop => None,
1706    }
1707}
1708
1709fn actual_move_places(
1710    stmt: &MirStatement,
1711    mir: &MirFunction,
1712    ownership_decisions: &HashMap<Point, OwnershipDecision>,
1713) -> Vec<Place> {
1714    match &stmt.kind {
1715        StatementKind::Assign(_, Rvalue::Use(Operand::Move(place)))
1716            if ownership_decisions.get(&stmt.point) == Some(&OwnershipDecision::Move) =>
1717        {
1718            vec![place.clone()]
1719        }
1720        StatementKind::Assign(_, Rvalue::Use(Operand::MoveExplicit(place)))
1721            if place_root_local_type(place, mir) != Some(LocalTypeInfo::Copy) =>
1722        {
1723            vec![place.clone()]
1724        }
1725        _ => Vec::new(),
1726    }
1727}
1728
1729fn place_root_local_type(place: &Place, mir: &MirFunction) -> Option<LocalTypeInfo> {
1730    mir.local_types.get(place.root_local().0 as usize).cloned()
1731}
1732
1733fn reinitializes_moved_place(dest_place: &Place, moved_place: &Place) -> bool {
1734    dest_place.is_prefix_of(moved_place)
1735}
1736
1737fn find_moved_place_conflict(
1738    moved_places: &HashMap<Place, shape_ast::ast::Span>,
1739    accessed_place: &Place,
1740) -> Option<(Place, shape_ast::ast::Span)> {
1741    moved_places
1742        .iter()
1743        .find(|(moved_place, _)| accessed_place.conflicts_with(moved_place))
1744        .map(|(place, span)| (place.clone(), *span))
1745}
1746
1747fn last_use_span_for_loan(facts: &BorrowFacts, loan_id: u32) -> Option<shape_ast::ast::Span> {
1748    facts
1749        .use_of_loan
1750        .iter()
1751        .filter(|(candidate, _)| *candidate == loan_id)
1752        .filter_map(|(_, point)| facts.point_spans.get(point).copied())
1753        .max_by_key(|span| span.start)
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758    use super::*;
1759    use shape_ast::ast::Span;
1760
1761    fn span() -> Span {
1762        Span { start: 0, end: 1 }
1763    }
1764
1765    fn make_stmt(kind: StatementKind, point: u32) -> MirStatement {
1766        MirStatement {
1767            kind,
1768            span: span(),
1769            point: Point(point),
1770        }
1771    }
1772
1773    fn make_terminator(kind: TerminatorKind) -> Terminator {
1774        Terminator { kind, span: span() }
1775    }
1776
1777    #[test]
1778    fn test_single_shared_borrow_no_error() {
1779        let mir = MirFunction {
1780            name: "test".to_string(),
1781            blocks: vec![BasicBlock {
1782                id: BasicBlockId(0),
1783                statements: vec![
1784                    // _0 = 42
1785                    make_stmt(
1786                        StatementKind::Assign(
1787                            Place::Local(SlotId(0)),
1788                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1789                        ),
1790                        0,
1791                    ),
1792                    // _1 = &_0
1793                    make_stmt(
1794                        StatementKind::Assign(
1795                            Place::Local(SlotId(1)),
1796                            Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(0))),
1797                        ),
1798                        1,
1799                    ),
1800                ],
1801                terminator: make_terminator(TerminatorKind::Return),
1802            }],
1803            num_locals: 2,
1804            param_slots: vec![],
1805            param_reference_kinds: vec![],
1806            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::NonCopy],
1807            span: span(),
1808        };
1809
1810        let analysis = analyze(&mir, &Default::default());
1811        assert!(analysis.errors.is_empty(), "expected no errors");
1812    }
1813
1814    #[test]
1815    fn test_conflicting_shared_and_exclusive_error() {
1816        // _0 = value
1817        // _1 = &_0 (shared)
1818        // _2 = &mut _0 (exclusive) — should conflict with _1
1819        let mir = MirFunction {
1820            name: "test".to_string(),
1821            blocks: vec![BasicBlock {
1822                id: BasicBlockId(0),
1823                statements: vec![
1824                    make_stmt(
1825                        StatementKind::Assign(
1826                            Place::Local(SlotId(0)),
1827                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1828                        ),
1829                        0,
1830                    ),
1831                    make_stmt(
1832                        StatementKind::Assign(
1833                            Place::Local(SlotId(1)),
1834                            Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(0))),
1835                        ),
1836                        1,
1837                    ),
1838                    make_stmt(
1839                        StatementKind::Assign(
1840                            Place::Local(SlotId(2)),
1841                            Rvalue::Borrow(BorrowKind::Exclusive, Place::Local(SlotId(0))),
1842                        ),
1843                        2,
1844                    ),
1845                ],
1846                terminator: make_terminator(TerminatorKind::Return),
1847            }],
1848            num_locals: 3,
1849            param_slots: vec![],
1850            param_reference_kinds: vec![],
1851            local_types: vec![
1852                LocalTypeInfo::NonCopy,
1853                LocalTypeInfo::NonCopy,
1854                LocalTypeInfo::NonCopy,
1855            ],
1856            span: span(),
1857        };
1858
1859        let analysis = analyze(&mir, &Default::default());
1860        assert!(
1861            !analysis.errors.is_empty(),
1862            "expected borrow conflict error"
1863        );
1864        assert_eq!(
1865            analysis.errors[0].kind,
1866            BorrowErrorKind::ConflictSharedExclusive
1867        );
1868    }
1869
1870    #[test]
1871    fn test_disjoint_field_borrows_no_conflict() {
1872        // _1 = &_0.a (shared)
1873        // _2 = &mut _0.b (exclusive) — disjoint fields, no conflict
1874        let mir = MirFunction {
1875            name: "test".to_string(),
1876            blocks: vec![BasicBlock {
1877                id: BasicBlockId(0),
1878                statements: vec![
1879                    make_stmt(
1880                        StatementKind::Assign(
1881                            Place::Local(SlotId(0)),
1882                            Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
1883                        ),
1884                        0,
1885                    ),
1886                    make_stmt(
1887                        StatementKind::Assign(
1888                            Place::Local(SlotId(1)),
1889                            Rvalue::Borrow(
1890                                BorrowKind::Shared,
1891                                Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(0)),
1892                            ),
1893                        ),
1894                        1,
1895                    ),
1896                    make_stmt(
1897                        StatementKind::Assign(
1898                            Place::Local(SlotId(2)),
1899                            Rvalue::Borrow(
1900                                BorrowKind::Exclusive,
1901                                Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(1)),
1902                            ),
1903                        ),
1904                        2,
1905                    ),
1906                ],
1907                terminator: make_terminator(TerminatorKind::Return),
1908            }],
1909            num_locals: 3,
1910            param_slots: vec![],
1911            param_reference_kinds: vec![],
1912            local_types: vec![
1913                LocalTypeInfo::NonCopy,
1914                LocalTypeInfo::NonCopy,
1915                LocalTypeInfo::NonCopy,
1916            ],
1917            span: span(),
1918        };
1919
1920        let analysis = analyze(&mir, &Default::default());
1921        assert!(
1922            analysis.errors.is_empty(),
1923            "disjoint field borrows should not conflict, got: {:?}",
1924            analysis.errors
1925        );
1926    }
1927
1928    #[test]
1929    fn test_read_while_exclusive_borrow_error() {
1930        let mir = MirFunction {
1931            name: "test".to_string(),
1932            blocks: vec![BasicBlock {
1933                id: BasicBlockId(0),
1934                statements: vec![
1935                    make_stmt(
1936                        StatementKind::Assign(
1937                            Place::Local(SlotId(0)),
1938                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1939                        ),
1940                        0,
1941                    ),
1942                    make_stmt(
1943                        StatementKind::Assign(
1944                            Place::Local(SlotId(1)),
1945                            Rvalue::Borrow(BorrowKind::Exclusive, Place::Local(SlotId(0))),
1946                        ),
1947                        1,
1948                    ),
1949                    make_stmt(
1950                        StatementKind::Assign(
1951                            Place::Local(SlotId(2)),
1952                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
1953                        ),
1954                        2,
1955                    ),
1956                ],
1957                terminator: make_terminator(TerminatorKind::Return),
1958            }],
1959            num_locals: 3,
1960            param_slots: vec![],
1961            param_reference_kinds: vec![],
1962            local_types: vec![
1963                LocalTypeInfo::NonCopy,
1964                LocalTypeInfo::NonCopy,
1965                LocalTypeInfo::NonCopy,
1966            ],
1967            span: span(),
1968        };
1969
1970        let analysis = analyze(&mir, &Default::default());
1971        assert!(
1972            analysis
1973                .errors
1974                .iter()
1975                .any(|error| error.kind == BorrowErrorKind::ReadWhileExclusivelyBorrowed),
1976            "expected read-while-exclusive error, got {:?}",
1977            analysis.errors
1978        );
1979    }
1980
1981    #[test]
1982    fn test_reference_escape_error_for_returned_ref_alias() {
1983        let mir = MirFunction {
1984            name: "test".to_string(),
1985            blocks: vec![BasicBlock {
1986                id: BasicBlockId(0),
1987                statements: vec![
1988                    make_stmt(
1989                        StatementKind::Assign(
1990                            Place::Local(SlotId(1)),
1991                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1992                        ),
1993                        0,
1994                    ),
1995                    make_stmt(
1996                        StatementKind::Assign(
1997                            Place::Local(SlotId(2)),
1998                            Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(1))),
1999                        ),
2000                        1,
2001                    ),
2002                    make_stmt(
2003                        StatementKind::Assign(
2004                            Place::Local(SlotId(3)),
2005                            Rvalue::Use(Operand::Move(Place::Local(SlotId(2)))),
2006                        ),
2007                        2,
2008                    ),
2009                    make_stmt(
2010                        StatementKind::Assign(
2011                            Place::Local(SlotId(0)),
2012                            Rvalue::Use(Operand::Move(Place::Local(SlotId(3)))),
2013                        ),
2014                        3,
2015                    ),
2016                ],
2017                terminator: make_terminator(TerminatorKind::Return),
2018            }],
2019            num_locals: 4,
2020            param_slots: vec![],
2021            param_reference_kinds: vec![],
2022            local_types: vec![
2023                LocalTypeInfo::NonCopy,
2024                LocalTypeInfo::NonCopy,
2025                LocalTypeInfo::NonCopy,
2026                LocalTypeInfo::NonCopy,
2027            ],
2028            span: span(),
2029        };
2030
2031        let analysis = analyze(&mir, &Default::default());
2032        assert!(
2033            analysis
2034                .errors
2035                .iter()
2036                .any(|error| error.kind == BorrowErrorKind::ReferenceEscape),
2037            "expected reference-escape error, got {:?}",
2038            analysis.errors
2039        );
2040    }
2041
2042    #[test]
2043    fn test_use_after_explicit_move_error() {
2044        let mir = MirFunction {
2045            name: "test".to_string(),
2046            blocks: vec![BasicBlock {
2047                id: BasicBlockId(0),
2048                statements: vec![
2049                    make_stmt(
2050                        StatementKind::Assign(
2051                            Place::Local(SlotId(0)),
2052                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2053                        ),
2054                        0,
2055                    ),
2056                    make_stmt(
2057                        StatementKind::Assign(
2058                            Place::Local(SlotId(1)),
2059                            Rvalue::Use(Operand::MoveExplicit(Place::Local(SlotId(0)))),
2060                        ),
2061                        1,
2062                    ),
2063                    make_stmt(
2064                        StatementKind::Assign(
2065                            Place::Local(SlotId(2)),
2066                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
2067                        ),
2068                        2,
2069                    ),
2070                ],
2071                terminator: make_terminator(TerminatorKind::Return),
2072            }],
2073            num_locals: 3,
2074            param_slots: vec![],
2075            param_reference_kinds: vec![],
2076            local_types: vec![
2077                LocalTypeInfo::NonCopy,
2078                LocalTypeInfo::NonCopy,
2079                LocalTypeInfo::NonCopy,
2080            ],
2081            span: span(),
2082        };
2083
2084        let analysis = analyze(&mir, &Default::default());
2085        assert!(
2086            analysis
2087                .errors
2088                .iter()
2089                .any(|error| error.kind == BorrowErrorKind::UseAfterMove),
2090            "expected use-after-move error, got {:?}",
2091            analysis.errors
2092        );
2093    }
2094
2095    #[test]
2096    fn test_move_vs_clone_decision() {
2097        // _0 = value (NonCopy)
2098        // _1 = move _0  (point 1 — _0 NOT live after → Move)
2099        let mir = MirFunction {
2100            name: "test".to_string(),
2101            blocks: vec![BasicBlock {
2102                id: BasicBlockId(0),
2103                statements: vec![
2104                    make_stmt(
2105                        StatementKind::Assign(
2106                            Place::Local(SlotId(0)),
2107                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2108                        ),
2109                        0,
2110                    ),
2111                    make_stmt(
2112                        StatementKind::Assign(
2113                            Place::Local(SlotId(1)),
2114                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2115                        ),
2116                        1,
2117                    ),
2118                ],
2119                terminator: make_terminator(TerminatorKind::Return),
2120            }],
2121            num_locals: 2,
2122            param_slots: vec![],
2123            param_reference_kinds: vec![],
2124            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::NonCopy],
2125            span: span(),
2126        };
2127
2128        let analysis = analyze(&mir, &Default::default());
2129        // _0 is not used after point 1, so decision should be Move
2130        assert_eq!(
2131            analysis.ownership_at(Point(1)),
2132            OwnershipDecision::Move,
2133            "source dead after → should be Move"
2134        );
2135    }
2136
2137    #[test]
2138    fn test_nll_borrow_scoping() {
2139        // NLL test: borrow ends at last use, not at lexical scope exit
2140        // bb0: _0 = value; _1 = &_0; (use _1 here); goto bb1
2141        // bb1: _2 = &mut _0 — should be OK because _1 is no longer used
2142        let mir = MirFunction {
2143            name: "test".to_string(),
2144            blocks: vec![
2145                BasicBlock {
2146                    id: BasicBlockId(0),
2147                    statements: vec![
2148                        make_stmt(
2149                            StatementKind::Assign(
2150                                Place::Local(SlotId(0)),
2151                                Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2152                            ),
2153                            0,
2154                        ),
2155                        make_stmt(
2156                            StatementKind::Assign(
2157                                Place::Local(SlotId(1)),
2158                                Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(0))),
2159                            ),
2160                            1,
2161                        ),
2162                        // Use _1
2163                        make_stmt(
2164                            StatementKind::Assign(
2165                                Place::Local(SlotId(3)),
2166                                Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2167                            ),
2168                            2,
2169                        ),
2170                    ],
2171                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
2172                },
2173                BasicBlock {
2174                    id: BasicBlockId(1),
2175                    statements: vec![
2176                        // _1 is no longer used here — shared borrow should be "dead"
2177                        // So taking &mut _0 should be OK
2178                        make_stmt(
2179                            StatementKind::Assign(
2180                                Place::Local(SlotId(2)),
2181                                Rvalue::Borrow(BorrowKind::Exclusive, Place::Local(SlotId(0))),
2182                            ),
2183                            3,
2184                        ),
2185                    ],
2186                    terminator: make_terminator(TerminatorKind::Return),
2187                },
2188            ],
2189            num_locals: 4,
2190            param_slots: vec![],
2191            param_reference_kinds: vec![],
2192            local_types: vec![
2193                LocalTypeInfo::NonCopy,
2194                LocalTypeInfo::NonCopy,
2195                LocalTypeInfo::NonCopy,
2196                LocalTypeInfo::NonCopy,
2197            ],
2198            span: span(),
2199        };
2200
2201        let analysis = analyze(&mir, &Default::default());
2202        // With NLL, the shared borrow on _0 ends after last use of _1 (point 2).
2203        // The exclusive borrow at point 3 should NOT conflict.
2204        // Note: our current solver propagates loan_live_at through cfg_edge
2205        // without checking if the loan is actually used. For full NLL we need
2206        // to intersect with "loan_used_at" — this is tracked as a known TODO.
2207        // For now, this test documents the current behavior.
2208        let _ = analysis;
2209    }
2210
2211    #[test]
2212    fn test_clone_decision_when_source_live_after() {
2213        // _0 = value (NonCopy)
2214        // _1 = move _0 (point 1 — _0 IS live after because _2 uses it)
2215        // _2 = move _0 (point 2 — _0 NOT live after → Move)
2216        let mir = MirFunction {
2217            name: "test".to_string(),
2218            blocks: vec![BasicBlock {
2219                id: BasicBlockId(0),
2220                statements: vec![
2221                    make_stmt(
2222                        StatementKind::Assign(
2223                            Place::Local(SlotId(0)),
2224                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2225                        ),
2226                        0,
2227                    ),
2228                    make_stmt(
2229                        StatementKind::Assign(
2230                            Place::Local(SlotId(1)),
2231                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2232                        ),
2233                        1,
2234                    ),
2235                    make_stmt(
2236                        StatementKind::Assign(
2237                            Place::Local(SlotId(2)),
2238                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2239                        ),
2240                        2,
2241                    ),
2242                ],
2243                terminator: make_terminator(TerminatorKind::Return),
2244            }],
2245            num_locals: 3,
2246            param_slots: vec![],
2247            param_reference_kinds: vec![],
2248            local_types: vec![
2249                LocalTypeInfo::NonCopy,
2250                LocalTypeInfo::NonCopy,
2251                LocalTypeInfo::NonCopy,
2252            ],
2253            span: span(),
2254        };
2255
2256        let analysis = analyze(&mir, &Default::default());
2257        // At point 1, _0 is still used at point 2, so it's live → Clone
2258        assert_eq!(
2259            analysis.ownership_at(Point(1)),
2260            OwnershipDecision::Clone,
2261            "source live after → should be Clone"
2262        );
2263        // At point 2, _0 is not used after → Move
2264        assert_eq!(
2265            analysis.ownership_at(Point(2)),
2266            OwnershipDecision::Move,
2267            "source dead after → should be Move"
2268        );
2269    }
2270
2271    #[test]
2272    fn test_copy_type_always_copy_decision() {
2273        // _0 = 42 (Copy type)
2274        // _1 = move _0 — but since _0 is Copy, decision should be Copy
2275        let mir = MirFunction {
2276            name: "test".to_string(),
2277            blocks: vec![BasicBlock {
2278                id: BasicBlockId(0),
2279                statements: vec![
2280                    make_stmt(
2281                        StatementKind::Assign(
2282                            Place::Local(SlotId(0)),
2283                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2284                        ),
2285                        0,
2286                    ),
2287                    make_stmt(
2288                        StatementKind::Assign(
2289                            Place::Local(SlotId(1)),
2290                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2291                        ),
2292                        1,
2293                    ),
2294                ],
2295                terminator: make_terminator(TerminatorKind::Return),
2296            }],
2297            num_locals: 2,
2298            param_slots: vec![],
2299            param_reference_kinds: vec![],
2300            local_types: vec![LocalTypeInfo::Copy, LocalTypeInfo::Copy],
2301            span: span(),
2302        };
2303
2304        let analysis = analyze(&mir, &Default::default());
2305        assert_eq!(
2306            analysis.ownership_at(Point(1)),
2307            OwnershipDecision::Copy,
2308            "Copy type → always Copy regardless of liveness"
2309        );
2310    }
2311
2312    // =========================================================================
2313    // compose_return_reference_summary unit tests
2314    // =========================================================================
2315
2316    #[test]
2317    fn test_compose_summary_identity() {
2318        // Both empty projections — identity composition
2319        let arg = ReturnReferenceSummary {
2320            param_index: 2,
2321            kind: BorrowKind::Shared,
2322            projection: Some(vec![]),
2323        };
2324        let callee = ReturnReferenceSummary {
2325            param_index: 0,
2326            kind: BorrowKind::Exclusive,
2327            projection: Some(vec![]),
2328        };
2329        let result = compose_return_reference_summary(&arg, &callee);
2330        assert_eq!(result.param_index, 2); // from arg
2331        assert_eq!(result.kind, BorrowKind::Exclusive); // from callee
2332        assert_eq!(result.projection, Some(vec![]));
2333    }
2334
2335    #[test]
2336    fn test_compose_summary_some_index_some_empty() {
2337        let arg = ReturnReferenceSummary {
2338            param_index: 0,
2339            kind: BorrowKind::Shared,
2340            projection: Some(vec![ProjectionStep::Index]),
2341        };
2342        let callee = ReturnReferenceSummary {
2343            param_index: 0,
2344            kind: BorrowKind::Shared,
2345            projection: Some(vec![]),
2346        };
2347        let result = compose_return_reference_summary(&arg, &callee);
2348        assert_eq!(result.projection, Some(vec![ProjectionStep::Index]));
2349    }
2350
2351    #[test]
2352    fn test_compose_summary_callee_field_loses_precision() {
2353        let arg = ReturnReferenceSummary {
2354            param_index: 0,
2355            kind: BorrowKind::Shared,
2356            projection: Some(vec![]),
2357        };
2358        let callee = ReturnReferenceSummary {
2359            param_index: 0,
2360            kind: BorrowKind::Shared,
2361            projection: Some(vec![ProjectionStep::Field(FieldIdx(0))]),
2362        };
2363        let result = compose_return_reference_summary(&arg, &callee);
2364        assert_eq!(result.projection, None); // Field loses precision
2365    }
2366
2367    #[test]
2368    fn test_compose_summary_callee_index_composes() {
2369        let arg = ReturnReferenceSummary {
2370            param_index: 1,
2371            kind: BorrowKind::Shared,
2372            projection: Some(vec![ProjectionStep::Index]),
2373        };
2374        let callee = ReturnReferenceSummary {
2375            param_index: 0,
2376            kind: BorrowKind::Exclusive,
2377            projection: Some(vec![ProjectionStep::Index]),
2378        };
2379        let result = compose_return_reference_summary(&arg, &callee);
2380        assert_eq!(result.param_index, 1);
2381        assert_eq!(result.kind, BorrowKind::Exclusive);
2382        assert_eq!(
2383            result.projection,
2384            Some(vec![ProjectionStep::Index, ProjectionStep::Index])
2385        );
2386    }
2387
2388    #[test]
2389    fn test_compose_summary_arg_none() {
2390        let arg = ReturnReferenceSummary {
2391            param_index: 0,
2392            kind: BorrowKind::Shared,
2393            projection: None, // precision already lost
2394        };
2395        let callee = ReturnReferenceSummary {
2396            param_index: 0,
2397            kind: BorrowKind::Shared,
2398            projection: Some(vec![]),
2399        };
2400        let result = compose_return_reference_summary(&arg, &callee);
2401        assert_eq!(result.projection, None);
2402    }
2403
2404    #[test]
2405    fn test_compose_summary_callee_none() {
2406        let arg = ReturnReferenceSummary {
2407            param_index: 0,
2408            kind: BorrowKind::Shared,
2409            projection: Some(vec![ProjectionStep::Index]),
2410        };
2411        let callee = ReturnReferenceSummary {
2412            param_index: 0,
2413            kind: BorrowKind::Exclusive,
2414            projection: None,
2415        };
2416        let result = compose_return_reference_summary(&arg, &callee);
2417        assert_eq!(result.projection, None);
2418    }
2419
2420    // =========================================================================
2421    // Solver-level call composition tests (synthetic MIR)
2422    // =========================================================================
2423
2424    #[test]
2425    fn test_call_composition_identity() {
2426        // fn identity(&x) { x }
2427        // Caller: param _1 (&ref), call identity(_1) → _2, return _2
2428        // With callee summary for "identity": param_index=0, kind=Shared, projection=Some([])
2429        let mir = MirFunction {
2430            name: "caller".to_string(),
2431            blocks: vec![
2432                BasicBlock {
2433                    id: BasicBlockId(0),
2434                    statements: vec![
2435                        MirStatement {
2436                            kind: StatementKind::Assign(
2437                                Place::Local(SlotId(2)),
2438                                Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2439                            ),
2440                            span: span(),
2441                            point: Point(0),
2442                        },
2443                    ],
2444                    terminator: Terminator {
2445                        kind: TerminatorKind::Call {
2446                            func: Operand::Constant(MirConstant::Function(
2447                                "identity".to_string(),
2448                            )),
2449                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2450                            destination: Place::Local(SlotId(3)),
2451                            next: BasicBlockId(1),
2452                        },
2453                        span: span(),
2454                    },
2455                },
2456                BasicBlock {
2457                    id: BasicBlockId(1),
2458                    statements: vec![MirStatement {
2459                        kind: StatementKind::Assign(
2460                            Place::Local(SlotId(0)),
2461                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(3)))),
2462                        ),
2463                        span: span(),
2464                        point: Point(1),
2465                    }],
2466                    terminator: Terminator {
2467                        kind: TerminatorKind::Return,
2468                        span: span(),
2469                    },
2470                },
2471            ],
2472            num_locals: 4,
2473            param_slots: vec![SlotId(1)],
2474            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2475            local_types: vec![
2476                LocalTypeInfo::NonCopy,
2477                LocalTypeInfo::NonCopy,
2478                LocalTypeInfo::NonCopy,
2479                LocalTypeInfo::NonCopy,
2480            ],
2481            span: span(),
2482        };
2483
2484        let mut callee_summaries = CalleeSummaries::new();
2485        callee_summaries.insert(
2486            "identity".to_string(),
2487            ReturnReferenceSummary {
2488                param_index: 0,
2489                kind: BorrowKind::Shared,
2490                projection: Some(vec![]),
2491            },
2492        );
2493
2494        let analysis = analyze(&mir, &callee_summaries);
2495        assert!(
2496            analysis.return_reference_summary.is_some(),
2497            "expected return reference summary from composed call"
2498        );
2499        let summary = analysis.return_reference_summary.unwrap();
2500        assert_eq!(summary.param_index, 0);
2501        assert_eq!(summary.kind, BorrowKind::Shared);
2502    }
2503
2504    #[test]
2505    fn test_call_composition_unknown_callee() {
2506        // Same as above but no callee summary → conservative (no return summary)
2507        let mir = MirFunction {
2508            name: "caller".to_string(),
2509            blocks: vec![
2510                BasicBlock {
2511                    id: BasicBlockId(0),
2512                    statements: vec![MirStatement {
2513                        kind: StatementKind::Assign(
2514                            Place::Local(SlotId(2)),
2515                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2516                        ),
2517                        span: span(),
2518                        point: Point(0),
2519                    }],
2520                    terminator: Terminator {
2521                        kind: TerminatorKind::Call {
2522                            func: Operand::Constant(MirConstant::Function(
2523                                "unknown_fn".to_string(),
2524                            )),
2525                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2526                            destination: Place::Local(SlotId(3)),
2527                            next: BasicBlockId(1),
2528                        },
2529                        span: span(),
2530                    },
2531                },
2532                BasicBlock {
2533                    id: BasicBlockId(1),
2534                    statements: vec![MirStatement {
2535                        kind: StatementKind::Assign(
2536                            Place::Local(SlotId(0)),
2537                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(3)))),
2538                        ),
2539                        span: span(),
2540                        point: Point(1),
2541                    }],
2542                    terminator: Terminator {
2543                        kind: TerminatorKind::Return,
2544                        span: span(),
2545                    },
2546                },
2547            ],
2548            num_locals: 4,
2549            param_slots: vec![SlotId(1)],
2550            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2551            local_types: vec![
2552                LocalTypeInfo::NonCopy,
2553                LocalTypeInfo::NonCopy,
2554                LocalTypeInfo::NonCopy,
2555                LocalTypeInfo::NonCopy,
2556            ],
2557            span: span(),
2558        };
2559
2560        let analysis = analyze(&mir, &Default::default());
2561        // Unknown callee → no return reference summary composed
2562        assert!(
2563            analysis.return_reference_summary.is_none(),
2564            "unknown callee should not produce return reference summary"
2565        );
2566    }
2567
2568    #[test]
2569    fn test_call_composition_indirect_call() {
2570        // Call via Method (not Function) → conservative
2571        let mir = MirFunction {
2572            name: "caller".to_string(),
2573            blocks: vec![
2574                BasicBlock {
2575                    id: BasicBlockId(0),
2576                    statements: vec![MirStatement {
2577                        kind: StatementKind::Assign(
2578                            Place::Local(SlotId(2)),
2579                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2580                        ),
2581                        span: span(),
2582                        point: Point(0),
2583                    }],
2584                    terminator: Terminator {
2585                        kind: TerminatorKind::Call {
2586                            func: Operand::Constant(MirConstant::Method(
2587                                "identity".to_string(),
2588                            )),
2589                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2590                            destination: Place::Local(SlotId(3)),
2591                            next: BasicBlockId(1),
2592                        },
2593                        span: span(),
2594                    },
2595                },
2596                BasicBlock {
2597                    id: BasicBlockId(1),
2598                    statements: vec![MirStatement {
2599                        kind: StatementKind::Assign(
2600                            Place::Local(SlotId(0)),
2601                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(3)))),
2602                        ),
2603                        span: span(),
2604                        point: Point(1),
2605                    }],
2606                    terminator: Terminator {
2607                        kind: TerminatorKind::Return,
2608                        span: span(),
2609                    },
2610                },
2611            ],
2612            num_locals: 4,
2613            param_slots: vec![SlotId(1)],
2614            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2615            local_types: vec![
2616                LocalTypeInfo::NonCopy,
2617                LocalTypeInfo::NonCopy,
2618                LocalTypeInfo::NonCopy,
2619                LocalTypeInfo::NonCopy,
2620            ],
2621            span: span(),
2622        };
2623
2624        let mut callee_summaries = CalleeSummaries::new();
2625        callee_summaries.insert(
2626            "identity".to_string(),
2627            ReturnReferenceSummary {
2628                param_index: 0,
2629                kind: BorrowKind::Shared,
2630                projection: Some(vec![]),
2631            },
2632        );
2633
2634        // Method call, not Function call → conservative even with summary present
2635        let analysis = analyze(&mir, &callee_summaries);
2636        assert!(
2637            analysis.return_reference_summary.is_none(),
2638            "indirect (Method) call should not compose return summary"
2639        );
2640    }
2641
2642    #[test]
2643    fn test_call_composition_chain() {
2644        // Two-deep: param _1 → call "inner"(_1) → _3, call "outer"(_3) → _4, return _4
2645        // inner: param_index=0, kind=Shared, projection=Some([])
2646        // outer: param_index=0, kind=Exclusive, projection=Some([])
2647        // Result: param_index=0 (traces to caller's param), kind=Exclusive (outer dictates)
2648        let mir = MirFunction {
2649            name: "caller".to_string(),
2650            blocks: vec![
2651                BasicBlock {
2652                    id: BasicBlockId(0),
2653                    statements: vec![MirStatement {
2654                        kind: StatementKind::Assign(
2655                            Place::Local(SlotId(2)),
2656                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2657                        ),
2658                        span: span(),
2659                        point: Point(0),
2660                    }],
2661                    terminator: Terminator {
2662                        kind: TerminatorKind::Call {
2663                            func: Operand::Constant(MirConstant::Function(
2664                                "inner".to_string(),
2665                            )),
2666                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2667                            destination: Place::Local(SlotId(3)),
2668                            next: BasicBlockId(1),
2669                        },
2670                        span: span(),
2671                    },
2672                },
2673                BasicBlock {
2674                    id: BasicBlockId(1),
2675                    statements: vec![MirStatement {
2676                        kind: StatementKind::Nop,
2677                        span: span(),
2678                        point: Point(1),
2679                    }],
2680                    terminator: Terminator {
2681                        kind: TerminatorKind::Call {
2682                            func: Operand::Constant(MirConstant::Function(
2683                                "outer".to_string(),
2684                            )),
2685                            args: vec![Operand::Copy(Place::Local(SlotId(3)))],
2686                            destination: Place::Local(SlotId(4)),
2687                            next: BasicBlockId(2),
2688                        },
2689                        span: span(),
2690                    },
2691                },
2692                BasicBlock {
2693                    id: BasicBlockId(2),
2694                    statements: vec![MirStatement {
2695                        kind: StatementKind::Assign(
2696                            Place::Local(SlotId(0)),
2697                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(4)))),
2698                        ),
2699                        span: span(),
2700                        point: Point(2),
2701                    }],
2702                    terminator: Terminator {
2703                        kind: TerminatorKind::Return,
2704                        span: span(),
2705                    },
2706                },
2707            ],
2708            num_locals: 5,
2709            param_slots: vec![SlotId(1)],
2710            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2711            local_types: vec![
2712                LocalTypeInfo::NonCopy,
2713                LocalTypeInfo::NonCopy,
2714                LocalTypeInfo::NonCopy,
2715                LocalTypeInfo::NonCopy,
2716                LocalTypeInfo::NonCopy,
2717            ],
2718            span: span(),
2719        };
2720
2721        let mut callee_summaries = CalleeSummaries::new();
2722        callee_summaries.insert(
2723            "inner".to_string(),
2724            ReturnReferenceSummary {
2725                param_index: 0,
2726                kind: BorrowKind::Shared,
2727                projection: Some(vec![]),
2728            },
2729        );
2730        callee_summaries.insert(
2731            "outer".to_string(),
2732            ReturnReferenceSummary {
2733                param_index: 0,
2734                kind: BorrowKind::Exclusive,
2735                projection: Some(vec![]),
2736            },
2737        );
2738
2739        let analysis = analyze(&mir, &callee_summaries);
2740        assert!(
2741            analysis.return_reference_summary.is_some(),
2742            "chained composition should produce return reference summary"
2743        );
2744        let summary = analysis.return_reference_summary.unwrap();
2745        assert_eq!(summary.param_index, 0, "should trace to outermost param");
2746        assert_eq!(
2747            summary.kind,
2748            BorrowKind::Exclusive,
2749            "outer callee dictates the kind"
2750        );
2751    }
2752}