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                } => {
394                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
395                        facts.closure_capture_loans.push((loan_id, stmt.span));
396                        facts.loan_sinks.push(LoanSink {
397                            loan_id,
398                            kind: LoanSinkKind::ClosureEnv,
399                            sink_slot: Some(*closure_slot),
400                            span: stmt.span,
401                        });
402                    }
403                }
404                StatementKind::ArrayStore {
405                    container_slot,
406                    operands,
407                } => {
408                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
409                        facts.array_store_loans.push((loan_id, stmt.span));
410                        facts.loan_sinks.push(LoanSink {
411                            loan_id,
412                            kind: LoanSinkKind::ArrayStore,
413                            sink_slot: Some(*container_slot),
414                            span: stmt.span,
415                        });
416                    }
417                }
418                StatementKind::ObjectStore {
419                    container_slot,
420                    operands,
421                    ..
422                } => {
423                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
424                        facts.object_store_loans.push((loan_id, stmt.span));
425                        facts.loan_sinks.push(LoanSink {
426                            loan_id,
427                            kind: LoanSinkKind::ObjectStore,
428                            sink_slot: Some(*container_slot),
429                            span: stmt.span,
430                        });
431                    }
432                }
433                StatementKind::EnumStore {
434                    container_slot,
435                    operands,
436                    variant_name: _,
437                } => {
438                    for loan_id in local_loans_from_operands(&slot_loans, operands) {
439                        facts.enum_store_loans.push((loan_id, stmt.span));
440                        facts.loan_sinks.push(LoanSink {
441                            loan_id,
442                            kind: LoanSinkKind::EnumStore,
443                            sink_slot: Some(*container_slot),
444                            span: stmt.span,
445                        });
446                    }
447                }
448                StatementKind::Nop => {}
449            }
450
451            for read_place in statement_read_places(&stmt.kind) {
452                facts
453                    .reads
454                    .push((stmt.point.0, read_place.clone(), stmt.span));
455                if let Place::Local(slot) = read_place {
456                    if let Some(loans) = slot_loans.get(&slot) {
457                        for loan_id in loans {
458                            facts.use_of_loan.push((*loan_id, stmt.point.0));
459                        }
460                    }
461                }
462            }
463        }
464
465        // Process Call terminators for borrow facts
466        if let TerminatorKind::Call { func, args, destination, .. } = &block.terminator.kind {
467            let call_point = block.statements.last().map(|s| s.point.0).unwrap_or(0);
468            // Track reads from func and args operands
469            let mut all_operands = vec![func];
470            all_operands.extend(args.iter());
471            for op in &all_operands {
472                if let Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) = op {
473                    if let Some(loans) = slot_loans.get(&place.root_local()) {
474                        for &loan_id in loans {
475                            facts.use_of_loan.push((loan_id, call_point));
476                        }
477                    }
478                }
479            }
480            // Destination write: clear provenance, then compose callee summary if available
481            let dest_slot = destination.root_local();
482            slot_loans.remove(&dest_slot);
483            slot_reference_origins.remove(&dest_slot);
484            slot_reference_summaries.remove(&dest_slot);
485
486            // Compose callee return summary into destination slot (summary-driven).
487            // Only compose for MirConstant::Function calls — indirect calls (closures,
488            // method dispatch) use conservative clearing.
489            if let Operand::Constant(MirConstant::Function(callee_name)) = func {
490                if let Some(callee_summary) = callee_summaries.get(callee_name.as_str()) {
491                    if let Some(arg_operand) = args.get(callee_summary.param_index) {
492                        if let Operand::Copy(arg_place)
493                        | Operand::Move(arg_place)
494                        | Operand::MoveExplicit(arg_place) = arg_operand
495                        {
496                            let arg_slot = arg_place.root_local();
497
498                            // Inherit loans from the argument slot
499                            if let Some(arg_loans) = slot_loans.get(&arg_slot).cloned() {
500                                slot_loans.insert(dest_slot, arg_loans);
501                            }
502
503                            // Compose reference summary (handles imprecision correctly)
504                            if let Some(arg_summary) =
505                                slot_reference_summaries.get(&arg_slot).cloned()
506                            {
507                                let composed = compose_return_reference_summary(
508                                    &arg_summary,
509                                    callee_summary,
510                                );
511
512                                // Only compose origin when projection precision is preserved.
513                                // Origin is always-precise (Vec, not Option<Vec>); if projection
514                                // loses precision the origin becomes meaningless.
515                                if composed.projection.is_some() {
516                                    if let Some((_, origin)) =
517                                        slot_reference_origins.get(&arg_slot).cloned()
518                                    {
519                                        // callee_proj is guaranteed Some and Field-free here
520                                        if let Some(ref callee_proj) = callee_summary.projection {
521                                            let mut proj = origin.projection.clone();
522                                            proj.extend(callee_proj.iter().copied());
523                                            slot_reference_origins.insert(
524                                                dest_slot,
525                                                (
526                                                    composed.kind,
527                                                    ReferenceOrigin {
528                                                        root: origin.root,
529                                                        projection: proj,
530                                                    },
531                                                ),
532                                            );
533                                        }
534                                    }
535                                    // Ref params seed summaries but NOT origins (solver.rs:106).
536                                    // If arg has summary but no origin, origin stays cleared.
537                                }
538                                // else: projection lost → origin stays cleared
539
540                                slot_reference_summaries.insert(dest_slot, composed);
541                            }
542                        }
543                    }
544                }
545            }
546        }
547    }
548
549    // Detect potential conflicts between loans on the same place
550    let loan_ids: Vec<u32> = facts.loan_info.keys().copied().collect();
551    for i in 0..loan_ids.len() {
552        for j in (i + 1)..loan_ids.len() {
553            let a = loan_ids[i];
554            let b = loan_ids[j];
555            let info_a = &facts.loan_info[&a];
556            let info_b = &facts.loan_info[&b];
557
558            // Two loans conflict if they borrow overlapping places and at least one is exclusive
559            if info_a.borrowed_place.conflicts_with(&info_b.borrowed_place)
560                && (info_a.kind == BorrowKind::Exclusive || info_b.kind == BorrowKind::Exclusive)
561            {
562                facts.potential_conflicts.push((a, b));
563            }
564        }
565    }
566
567    facts
568}
569
570fn operand_read_places<'a>(operand: &'a Operand, reads: &mut Vec<Place>) {
571    match operand {
572        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
573            reads.push(place.clone());
574            place_nested_read_places(place, reads);
575        }
576        Operand::Constant(_) => {}
577    }
578}
579
580fn place_nested_read_places(place: &Place, reads: &mut Vec<Place>) {
581    match place {
582        Place::Local(_) => {}
583        Place::Field(base, _) | Place::Deref(base) => {
584            place_nested_read_places(base, reads);
585        }
586        Place::Index(base, index) => {
587            place_nested_read_places(base, reads);
588            operand_read_places(index, reads);
589        }
590    }
591}
592
593fn statement_read_places(kind: &StatementKind) -> Vec<Place> {
594    let mut reads = Vec::new();
595    match kind {
596        StatementKind::Assign(_, rvalue) => match rvalue {
597            Rvalue::Use(operand) | Rvalue::Clone(operand) => {
598                operand_read_places(operand, &mut reads)
599            }
600            Rvalue::Borrow(_, _) => {}
601            Rvalue::BinaryOp(_, lhs, rhs) => {
602                operand_read_places(lhs, &mut reads);
603                operand_read_places(rhs, &mut reads);
604            }
605            Rvalue::UnaryOp(_, operand) => operand_read_places(operand, &mut reads),
606            Rvalue::Aggregate(operands) => {
607                for operand in operands {
608                    operand_read_places(operand, &mut reads);
609                }
610            }
611            Rvalue::EnumTest { operand, .. }
612            | Rvalue::EnumPayload { operand, .. }
613            | Rvalue::TypePatternTest { operand, .. }
614            | Rvalue::EnumDiscriminantTest { operand, .. } => {
615                operand_read_places(operand, &mut reads);
616            }
617        },
618        StatementKind::Drop(place) => place_nested_read_places(place, &mut reads),
619        StatementKind::TaskBoundary(operands, _kind) => {
620            for operand in operands {
621                operand_read_places(operand, &mut reads);
622            }
623        }
624        StatementKind::ClosureCapture { operands, .. } => {
625            for operand in operands {
626                operand_read_places(operand, &mut reads);
627            }
628        }
629        StatementKind::ArrayStore { operands, .. } => {
630            for operand in operands {
631                operand_read_places(operand, &mut reads);
632            }
633        }
634        StatementKind::ObjectStore { operands, .. } => {
635            for operand in operands {
636                operand_read_places(operand, &mut reads);
637            }
638        }
639        StatementKind::EnumStore { operands, .. } => {
640            for operand in operands {
641                operand_read_places(operand, &mut reads);
642            }
643        }
644        StatementKind::Nop => {}
645    }
646    reads
647}
648
649fn local_loans_from_operand(slot_loans: &HashMap<SlotId, Vec<u32>>, operand: &Operand) -> Vec<u32> {
650    match operand {
651        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => slot_loans
652            .get(&place.root_local())
653            .cloned()
654            .unwrap_or_default(),
655        Operand::Constant(_) => Vec::new(),
656    }
657}
658
659fn local_loans_from_operands(
660    slot_loans: &HashMap<SlotId, Vec<u32>>,
661    operands: &[Operand],
662) -> Vec<u32> {
663    let mut loans = Vec::new();
664    let mut seen = HashSet::new();
665    for operand in operands {
666        for loan in local_loans_from_operand(slot_loans, operand) {
667            if seen.insert(loan) {
668                loans.push(loan);
669            }
670        }
671    }
672    loans
673}
674
675fn update_slot_loan_aliases(
676    slot_loans: &mut HashMap<SlotId, Vec<u32>>,
677    dest_slot: SlotId,
678    rvalue: &Rvalue,
679) {
680    match rvalue {
681        Rvalue::Borrow(_, _) => {}
682        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
683        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
684        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
685        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
686        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
687            if let Some(loans) = slot_loans.get(src_slot).cloned() {
688                slot_loans.insert(dest_slot, loans);
689            } else {
690                slot_loans.remove(&dest_slot);
691            }
692        }
693        _ => {
694            slot_loans.remove(&dest_slot);
695        }
696    }
697}
698
699fn local_loans_from_rvalue(slot_loans: &HashMap<SlotId, Vec<u32>>, rvalue: &Rvalue) -> Vec<u32> {
700    match rvalue {
701        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
702        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
703        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
704        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
705        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
706            slot_loans.get(src_slot).cloned().unwrap_or_default()
707        }
708        _ => Vec::new(),
709    }
710}
711
712fn update_slot_reference_summaries(
713    slot_reference_summaries: &mut HashMap<SlotId, ReturnReferenceSummary>,
714    dest_slot: SlotId,
715    rvalue: &Rvalue,
716) {
717    match rvalue {
718        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
719        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
720        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
721        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
722        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
723            if let Some(contract) = slot_reference_summaries.get(src_slot).cloned() {
724                slot_reference_summaries.insert(dest_slot, contract);
725            } else {
726                slot_reference_summaries.remove(&dest_slot);
727            }
728        }
729        _ => {
730            slot_reference_summaries.remove(&dest_slot);
731        }
732    }
733}
734
735fn reference_summary_from_rvalue(
736    slot_reference_summaries: &HashMap<SlotId, ReturnReferenceSummary>,
737    rvalue: &Rvalue,
738) -> Option<ReturnReferenceSummary> {
739    match rvalue {
740        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
741        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
742        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
743        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
744        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
745            slot_reference_summaries.get(src_slot).cloned()
746        }
747        _ => None,
748    }
749}
750
751fn update_slot_reference_origins(
752    slot_reference_origins: &mut HashMap<SlotId, (BorrowKind, ReferenceOrigin)>,
753    dest_slot: SlotId,
754    rvalue: &Rvalue,
755) {
756    match rvalue {
757        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
758        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
759        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
760        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
761        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
762            if let Some(origin) = slot_reference_origins.get(src_slot).cloned() {
763                slot_reference_origins.insert(dest_slot, origin);
764            } else {
765                slot_reference_origins.remove(&dest_slot);
766            }
767        }
768        _ => {
769            slot_reference_origins.remove(&dest_slot);
770        }
771    }
772}
773
774fn reference_origin_from_rvalue(
775    slot_reference_origins: &HashMap<SlotId, (BorrowKind, ReferenceOrigin)>,
776    rvalue: &Rvalue,
777) -> Option<(BorrowKind, ReferenceOrigin)> {
778    match rvalue {
779        Rvalue::Borrow(kind, place) => Some((
780            *kind,
781            reference_origin_for_place(place, &[]),
782        )),
783        Rvalue::Use(Operand::Copy(Place::Local(src_slot)))
784        | Rvalue::Use(Operand::Move(Place::Local(src_slot)))
785        | Rvalue::Use(Operand::MoveExplicit(Place::Local(src_slot)))
786        | Rvalue::Clone(Operand::Copy(Place::Local(src_slot)))
787        | Rvalue::Clone(Operand::Move(Place::Local(src_slot))) => {
788            slot_reference_origins.get(src_slot).cloned()
789        }
790        _ => None,
791    }
792}
793
794fn reference_origin_for_place(place: &Place, param_slots: &[SlotId]) -> ReferenceOrigin {
795    let root_slot = place.root_local();
796    let root = param_slots
797        .iter()
798        .position(|slot| *slot == root_slot)
799        .map(ReferenceOriginRoot::Param)
800        .unwrap_or(ReferenceOriginRoot::Local(root_slot));
801    ReferenceOrigin {
802        root,
803        projection: place.projection_steps(),
804    }
805}
806
807fn reference_summary_from_origin(
808    borrow_kind: BorrowKind,
809    origin: &ReferenceOrigin,
810) -> Option<ReturnReferenceSummary> {
811    match origin.root {
812        ReferenceOriginRoot::Param(param_index) => Some(ReturnReferenceSummary {
813            param_index,
814            kind: borrow_kind,
815            projection: Some(origin.projection.clone()),
816        }),
817        ReferenceOriginRoot::Local(_) => None,
818    }
819}
820
821fn safe_reference_summary_for_borrow(
822    borrow_kind: BorrowKind,
823    borrowed_place: &Place,
824    param_reference_summaries: &HashMap<SlotId, ReturnReferenceSummary>,
825) -> Option<ReturnReferenceSummary> {
826    // Support both direct param borrows (&param) and field-of-param borrows (&param.field).
827    // The root local must be a parameter with a reference summary.
828    let param_summary = param_reference_summaries.get(&borrowed_place.root_local())?;
829    Some(ReturnReferenceSummary {
830        param_index: param_summary.param_index,
831        kind: borrow_kind,
832        projection: Some(borrowed_place.projection_steps()),
833    })
834}
835
836/// Compose a callee's return summary with the argument slot's existing summary.
837///
838/// - `param_index`: from `arg_summary` (traces to the caller's parameter)
839/// - `kind`: from `callee_summary` (callee dictates the returned borrow kind)
840/// - `projection`: concatenate only when BOTH are `Some` AND the callee
841///   projection contains no `Field` steps (FieldIdx is per-MirBuilder,
842///   not cross-function stable). Otherwise `None` (precision lost).
843fn compose_return_reference_summary(
844    arg_summary: &ReturnReferenceSummary,
845    callee_summary: &ReturnReferenceSummary,
846) -> ReturnReferenceSummary {
847    let projection = match (&arg_summary.projection, &callee_summary.projection) {
848        (Some(arg_proj), Some(callee_proj)) => {
849            if callee_proj
850                .iter()
851                .any(|step| matches!(step, ProjectionStep::Field(_)))
852            {
853                None // FieldIdx is per-MirBuilder, unsound across functions
854            } else {
855                let mut composed = arg_proj.clone();
856                composed.extend(callee_proj.iter().copied());
857                Some(composed)
858            }
859        }
860        _ => None, // precision already lost on one side
861    };
862    ReturnReferenceSummary {
863        param_index: arg_summary.param_index,
864        kind: callee_summary.kind,
865        projection,
866    }
867}
868
869fn resolve_return_reference_summary(
870    errors: &mut Vec<BorrowError>,
871    facts: &BorrowFacts,
872    loans_at_point: &HashMap<Point, Vec<LoanId>>,
873) -> Option<ReturnReferenceSummary> {
874    let mut merged_candidate: Option<ReturnReferenceSummary> = None;
875    let mut inconsistent = false;
876    for (candidate, _) in &facts.return_reference_candidates {
877        if let Some(existing) = merged_candidate.as_mut() {
878            if existing.param_index != candidate.param_index || existing.kind != candidate.kind {
879                inconsistent = true;
880                break;
881            }
882            if existing.projection != candidate.projection {
883                existing.projection = None;
884            }
885        } else {
886            merged_candidate = Some(candidate.clone());
887        }
888    }
889
890    if merged_candidate.is_none() {
891        return None;
892    }
893
894    let error_span = if inconsistent {
895        facts
896            .return_reference_candidates
897            .get(1)
898            .map(|(_, span)| *span)
899    } else {
900        facts.non_reference_return_spans.first().copied()
901    };
902
903    if let Some(span) = error_span {
904        let (conflicting_loan, loan_span, last_use_span) = facts
905            .return_reference_candidates
906            .first()
907            .and_then(|(candidate, candidate_span)| {
908                find_matching_loan_for_return_candidate(
909                    candidate,
910                    *candidate_span,
911                    facts,
912                    loans_at_point,
913                )
914            })
915            .unwrap_or((LoanId(0), span, None));
916        errors.push(BorrowError {
917            kind: BorrowErrorKind::InconsistentReferenceReturn,
918            span,
919            conflicting_loan,
920            loan_span,
921            last_use_span,
922            repairs: Vec::new(),
923        });
924        return None;
925    }
926
927    merged_candidate
928}
929
930fn find_matching_loan_for_return_candidate(
931    candidate: &ReturnReferenceSummary,
932    candidate_span: shape_ast::ast::Span,
933    facts: &BorrowFacts,
934    loans_at_point: &HashMap<Point, Vec<LoanId>>,
935) -> Option<(LoanId, shape_ast::ast::Span, Option<shape_ast::ast::Span>)> {
936    let point = facts
937        .point_spans
938        .iter()
939        .find_map(|(point, span)| (*span == candidate_span).then_some(Point(*point)))?;
940    let loans = loans_at_point.get(&point)?;
941    for loan in loans {
942        let info = facts.loan_info.get(&loan.0)?;
943        if info.kind == candidate.kind {
944            return Some((*loan, info.span, last_use_span_for_loan(facts, loan.0)));
945        }
946    }
947    None
948}
949
950/// Run the Datafrog solver to compute loan liveness and detect errors.
951pub fn solve(facts: &BorrowFacts) -> SolverResult {
952    let mut iteration = Iteration::new();
953
954    // Input relations (static — known before iteration)
955    // cfg_edge indexed by source point: (point1, point2)
956    let cfg_edge: Relation<(u32, u32)> = facts.cfg_edge.iter().cloned().collect();
957    // invalidates indexed by (point, loan)
958    let invalidates_set: std::collections::HashSet<(u32, u32)> =
959        facts.invalidates.iter().cloned().collect();
960
961    // Derived relation: loan_live_at(point, loan)
962    // Keyed by point for efficient join with cfg_edge.
963    let loan_live_at = iteration.variable::<(u32, u32)>("loan_live_at");
964
965    // Seed: a loan is live at the point where it's issued.
966    // Reindex from (loan, point) to (point, loan).
967    let seed: Vec<(u32, u32)> = facts
968        .loan_issued_at
969        .iter()
970        .map(|&(loan, point)| (point, loan))
971        .collect();
972    loan_live_at.extend(seed.iter().cloned());
973
974    // Fixed-point iteration:
975    // loan_live_at(point2, loan) :-
976    //   loan_live_at(point1, loan),
977    //   cfg_edge(point1, point2),
978    //   !invalidates(point1, loan).
979    while iteration.changed() {
980        // For each (point1, loan) in loan_live_at,
981        // join with cfg_edge on point1 to get point2,
982        // filter out if invalidates(point1, loan).
983        loan_live_at.from_leapjoin(
984            &loan_live_at,
985            cfg_edge.extend_with(|&(point1, _loan)| point1),
986            |&(point1, loan), &point2| {
987                if invalidates_set.contains(&(point1, loan)) {
988                    // Loan is invalidated at point1 — keep it live at point1,
989                    // but don't propagate it to successors.
990                    (u32::MAX, u32::MAX) // sentinel that won't match anything useful
991                } else {
992                    (point2, loan)
993                }
994            },
995        );
996    }
997
998    // Collect results and filter out sentinel values
999    let forward_live_points: Vec<(u32, u32)> = loan_live_at
1000        .complete()
1001        .iter()
1002        .filter(|&&(p, l)| p != u32::MAX && l != u32::MAX)
1003        .cloned()
1004        .collect();
1005    let (nll_live_set, loans_with_reachable_uses) = compute_nll_live_points(facts);
1006    let loan_live_at_result: Vec<(u32, u32)> = forward_live_points
1007        .into_iter()
1008        .filter(|point_loan| {
1009            !loans_with_reachable_uses.contains(&point_loan.1) || nll_live_set.contains(point_loan)
1010        })
1011        .collect();
1012
1013    // Build point → active loans map
1014    let mut loans_at_point: HashMap<Point, Vec<LoanId>> = HashMap::new();
1015    for &(point, loan) in &loan_live_at_result {
1016        loans_at_point
1017            .entry(Point(point))
1018            .or_default()
1019            .push(LoanId(loan));
1020    }
1021
1022    // Build loan → set of points for quick intersection queries
1023    let mut loan_points: HashMap<u32, std::collections::HashSet<u32>> = HashMap::new();
1024    for &(point, loan) in &loan_live_at_result {
1025        loan_points.entry(loan).or_default().insert(point);
1026    }
1027
1028    // Detect errors: two conflicting loans alive at the same point
1029    let mut errors = Vec::new();
1030    let mut seen_conflicts = std::collections::HashSet::new();
1031    for &(loan_a, loan_b) in &facts.potential_conflicts {
1032        let key = (loan_a.min(loan_b), loan_a.max(loan_b));
1033        if !seen_conflicts.insert(key) {
1034            continue;
1035        }
1036
1037        let points_a = loan_points.get(&loan_a);
1038        let points_b = loan_points.get(&loan_b);
1039
1040        if let (Some(pa), Some(pb)) = (points_a, points_b) {
1041            // Check if there's any intersection
1042            let has_overlap = pa.iter().any(|p| pb.contains(p));
1043            if has_overlap {
1044                let info_a = &facts.loan_info[&loan_a];
1045                let info_b = &facts.loan_info[&loan_b];
1046                let kind = if info_a.kind == BorrowKind::Exclusive
1047                    && info_b.kind == BorrowKind::Exclusive
1048                {
1049                    BorrowErrorKind::ConflictExclusiveExclusive
1050                } else {
1051                    BorrowErrorKind::ConflictSharedExclusive
1052                };
1053                errors.push(BorrowError {
1054                    kind,
1055                    span: info_b.span,
1056                    conflicting_loan: LoanId(loan_a),
1057                    loan_span: info_a.span,
1058                    last_use_span: last_use_span_for_loan(facts, loan_a),
1059                    repairs: Vec::new(),
1060                });
1061            }
1062        }
1063    }
1064
1065    let mut seen_writes = std::collections::HashSet::new();
1066    for (point, place, span) in &facts.writes {
1067        let point_key = Point(*point);
1068        let Some(loans) = loans_at_point.get(&point_key) else {
1069            continue;
1070        };
1071        for loan in loans {
1072            let info = &facts.loan_info[&loan.0];
1073            if !place.conflicts_with(&info.borrowed_place) {
1074                continue;
1075            }
1076            let key = (*point, loan.0);
1077            if !seen_writes.insert(key) {
1078                continue;
1079            }
1080            errors.push(BorrowError {
1081                kind: BorrowErrorKind::WriteWhileBorrowed,
1082                span: *span,
1083                conflicting_loan: *loan,
1084                loan_span: info.span,
1085                last_use_span: last_use_span_for_loan(facts, loan.0),
1086                repairs: Vec::new(),
1087            });
1088            break;
1089        }
1090    }
1091
1092    let mut seen_reads = std::collections::HashSet::new();
1093    for (point, place, span) in &facts.reads {
1094        let point_key = Point(*point);
1095        let Some(loans) = loans_at_point.get(&point_key) else {
1096            continue;
1097        };
1098        for loan in loans {
1099            let info = &facts.loan_info[&loan.0];
1100            if info.kind != BorrowKind::Exclusive || !place.conflicts_with(&info.borrowed_place) {
1101                continue;
1102            }
1103            let key = (*point, loan.0);
1104            if !seen_reads.insert(key) {
1105                continue;
1106            }
1107            errors.push(BorrowError {
1108                kind: BorrowErrorKind::ReadWhileExclusivelyBorrowed,
1109                span: *span,
1110                conflicting_loan: *loan,
1111                loan_span: info.span,
1112                last_use_span: last_use_span_for_loan(facts, loan.0),
1113                repairs: Vec::new(),
1114            });
1115            break;
1116        }
1117    }
1118
1119    let mut seen_escapes = std::collections::HashSet::new();
1120    for (loan_id, span) in &facts.escaped_loans {
1121        if !seen_escapes.insert((*loan_id, span.start, span.end)) {
1122            continue;
1123        }
1124        let info = &facts.loan_info[loan_id];
1125        errors.push(BorrowError {
1126            kind: BorrowErrorKind::ReferenceEscape,
1127            span: *span,
1128            conflicting_loan: LoanId(*loan_id),
1129            loan_span: info.span,
1130            last_use_span: last_use_span_for_loan(facts, *loan_id),
1131            repairs: Vec::new(),
1132        });
1133    }
1134
1135    let mut seen_sinks = std::collections::HashSet::new();
1136    for sink in &facts.loan_sinks {
1137        let key = (
1138            sink.loan_id,
1139            sink.kind,
1140            sink.span.start,
1141            sink.span.end,
1142            sink.sink_slot.map(|slot| slot.0),
1143        );
1144        if !seen_sinks.insert(key) {
1145            continue;
1146        }
1147
1148        let info = &facts.loan_info[&sink.loan_id];
1149        let sink_is_local = sink
1150            .sink_slot
1151            .and_then(|slot| facts.slot_escape_status.get(&slot).copied())
1152            == Some(EscapeStatus::Local);
1153
1154        let kind = match sink.kind {
1155            LoanSinkKind::ReturnSlot => continue,
1156            LoanSinkKind::ClosureEnv if sink_is_local => continue,
1157            LoanSinkKind::ClosureEnv => BorrowErrorKind::ReferenceEscapeIntoClosure,
1158            // Phase D: exclusive loan registered via `ClosureCapture` for a
1159            // non-escaping closure slot whose mutable capture root is marked
1160            // `LocalMutablePtr`. The loan is issued purely for solver
1161            // bookkeeping so outer reads/writes during the closure's lifetime
1162            // are caught by the standard exclusive-loan rules; the sink itself
1163            // is local by construction (non-escaping closure) and should never
1164            // synthesize a diagnostic.
1165            LoanSinkKind::ClosureEnvMut => continue,
1166            LoanSinkKind::ArrayStore | LoanSinkKind::ArrayAssignment if sink_is_local => continue,
1167            LoanSinkKind::ArrayStore | LoanSinkKind::ArrayAssignment => {
1168                BorrowErrorKind::ReferenceStoredInArray
1169            }
1170            LoanSinkKind::ObjectStore | LoanSinkKind::ObjectAssignment if sink_is_local => continue,
1171            LoanSinkKind::ObjectStore | LoanSinkKind::ObjectAssignment => {
1172                BorrowErrorKind::ReferenceStoredInObject
1173            }
1174            LoanSinkKind::EnumStore if sink_is_local => continue,
1175            LoanSinkKind::EnumStore => BorrowErrorKind::ReferenceStoredInEnum,
1176            LoanSinkKind::StructuredTaskBoundary => {
1177                BorrowErrorKind::ExclusiveRefAcrossTaskBoundary
1178            }
1179            LoanSinkKind::DetachedTaskBoundary if info.kind == BorrowKind::Exclusive => {
1180                BorrowErrorKind::ExclusiveRefAcrossTaskBoundary
1181            }
1182            LoanSinkKind::DetachedTaskBoundary => BorrowErrorKind::SharedRefAcrossDetachedTask,
1183        };
1184
1185        errors.push(BorrowError {
1186            kind,
1187            span: sink.span,
1188            conflicting_loan: LoanId(sink.loan_id),
1189            loan_span: info.span,
1190            last_use_span: last_use_span_for_loan(facts, sink.loan_id),
1191            repairs: Vec::new(),
1192        });
1193    }
1194
1195    // Non-sendable values across detached task boundaries
1196    let mut seen_non_sendable = std::collections::HashSet::new();
1197    for (slot_id, span) in &facts.non_sendable_task_boundary {
1198        if !seen_non_sendable.insert((*slot_id, span.start, span.end)) {
1199            continue;
1200        }
1201        errors.push(BorrowError {
1202            kind: BorrowErrorKind::NonSendableAcrossTaskBoundary,
1203            span: *span,
1204            conflicting_loan: LoanId(0),
1205            loan_span: *span,
1206            last_use_span: None,
1207            repairs: Vec::new(),
1208        });
1209    }
1210
1211    let return_reference_summary =
1212        resolve_return_reference_summary(&mut errors, facts, &loans_at_point);
1213
1214    SolverResult {
1215        loans_at_point,
1216        errors,
1217        loan_info: facts.loan_info.clone(),
1218        return_reference_summary,
1219    }
1220}
1221
1222fn compute_nll_live_points(facts: &BorrowFacts) -> (HashSet<(u32, u32)>, HashSet<u32>) {
1223    let mut predecessors: HashMap<u32, Vec<u32>> = HashMap::new();
1224    for (from, to) in &facts.cfg_edge {
1225        predecessors.entry(*to).or_default().push(*from);
1226    }
1227
1228    let issue_points: HashMap<u32, u32> = facts
1229        .loan_issued_at
1230        .iter()
1231        .map(|(loan_id, point)| (*loan_id, *point))
1232        .collect();
1233
1234    let mut invalidation_points: HashMap<u32, HashSet<u32>> = HashMap::new();
1235    for (point, loan_id) in &facts.invalidates {
1236        invalidation_points
1237            .entry(*loan_id)
1238            .or_default()
1239            .insert(*point);
1240    }
1241
1242    let mut use_points: HashMap<u32, Vec<u32>> = HashMap::new();
1243    for (loan_id, point) in &facts.use_of_loan {
1244        use_points.entry(*loan_id).or_default().push(*point);
1245    }
1246
1247    let mut live_points = HashSet::new();
1248    let mut loans_with_reachable_uses = HashSet::new();
1249    for (loan_id, issue_point) in issue_points {
1250        let mut worklist = use_points.get(&loan_id).cloned().unwrap_or_default();
1251        let invalidates = invalidation_points.get(&loan_id);
1252        let mut visited = HashSet::new();
1253        let mut loan_live_points = HashSet::new();
1254        let mut reached_issue = false;
1255
1256        while let Some(point) = worklist.pop() {
1257            if !visited.insert(point) {
1258                continue;
1259            }
1260
1261            loan_live_points.insert((point, loan_id));
1262
1263            if point == issue_point {
1264                reached_issue = true;
1265                continue;
1266            }
1267
1268            if invalidates.is_some_and(|points| points.contains(&point)) {
1269                continue;
1270            }
1271
1272            if let Some(preds) = predecessors.get(&point) {
1273                worklist.extend(preds.iter().copied());
1274            }
1275        }
1276
1277        if reached_issue {
1278            loans_with_reachable_uses.insert(loan_id);
1279            live_points.extend(loan_live_points);
1280        }
1281    }
1282
1283    (live_points, loans_with_reachable_uses)
1284}
1285
1286/// Raw solver output (before combining with liveness for full BorrowAnalysis).
1287#[derive(Debug)]
1288pub struct SolverResult {
1289    pub loans_at_point: HashMap<Point, Vec<LoanId>>,
1290    pub errors: Vec<BorrowError>,
1291    pub loan_info: HashMap<u32, LoanInfo>,
1292    pub return_reference_summary: Option<ReturnReferenceSummary>,
1293}
1294
1295/// Run the complete borrow analysis pipeline for a MIR function.
1296/// This is the main entry point — produces the single BorrowAnalysis
1297/// consumed by compiler, LSP, and diagnostics.
1298/// Extract a borrow summary for a function — describes which parameters are
1299/// borrowed and which parameter pairs must not alias at call sites.
1300pub fn extract_borrow_summary(
1301    mir: &MirFunction,
1302    return_summary: Option<ReturnReferenceSummary>,
1303) -> FunctionBorrowSummary {
1304    let num_params = mir.param_slots.len();
1305    let mut param_borrows: Vec<Option<BorrowKind>> = mir
1306        .param_reference_kinds
1307        .iter()
1308        .cloned()
1309        .collect();
1310    // Pad to num_params if param_reference_kinds is shorter
1311    while param_borrows.len() < num_params {
1312        param_borrows.push(None);
1313    }
1314
1315    // Determine which params are written to (mutated) in the function body
1316    let mut mutated_params: HashSet<usize> = HashSet::new();
1317    let mut read_params: HashSet<usize> = HashSet::new();
1318    for block in mir.iter_blocks() {
1319        for stmt in &block.statements {
1320            match &stmt.kind {
1321                StatementKind::Assign(dest, rvalue) => {
1322                    // Check if dest's root is a parameter (handles Local, Field, Index)
1323                    let root = dest.root_local();
1324                    if let Some(param_idx) = mir.param_slots.iter().position(|s| *s == root) {
1325                        mutated_params.insert(param_idx);
1326                    }
1327                    // Check if any param is read in the rvalue
1328                    for param_idx in 0..num_params {
1329                        if rvalue_uses_param(rvalue, mir.param_slots[param_idx]) {
1330                            read_params.insert(param_idx);
1331                        }
1332                    }
1333                }
1334                _ => {}
1335            }
1336        }
1337        // Check terminator args for reads
1338        if let TerminatorKind::Call { args, .. } = &block.terminator.kind {
1339            for arg in args {
1340                for param_idx in 0..num_params {
1341                    if operand_uses_param(arg, mir.param_slots[param_idx]) {
1342                        read_params.insert(param_idx);
1343                    }
1344                }
1345            }
1346        }
1347    }
1348
1349    // Compute effective borrow kind per param: explicit annotations take priority,
1350    // otherwise infer from usage — mutated → Exclusive, read → Shared.
1351    let mut effective_borrows: Vec<Option<BorrowKind>> = param_borrows.clone();
1352    for idx in 0..num_params {
1353        if effective_borrows[idx].is_none() {
1354            if mutated_params.contains(&idx) {
1355                effective_borrows[idx] = Some(BorrowKind::Exclusive);
1356            } else if read_params.contains(&idx) {
1357                effective_borrows[idx] = Some(BorrowKind::Shared);
1358            }
1359        }
1360    }
1361
1362    // Build conflict pairs: a mutated param conflicts with every other param
1363    // that is read or borrowed (shared or exclusive).
1364    let mut conflict_pairs = Vec::new();
1365    for &mutated_idx in &mutated_params {
1366        for other_idx in 0..num_params {
1367            if other_idx == mutated_idx {
1368                continue;
1369            }
1370            // Mutated param conflicts with any other param that is used
1371            if effective_borrows[other_idx].is_some() {
1372                conflict_pairs.push((mutated_idx, other_idx));
1373            }
1374        }
1375    }
1376    // Also: two exclusive borrows on different params always conflict
1377    for i in 0..num_params {
1378        for j in (i + 1)..num_params {
1379            if effective_borrows[i] == Some(BorrowKind::Exclusive)
1380                && effective_borrows[j] == Some(BorrowKind::Exclusive)
1381                && !conflict_pairs.contains(&(i, j))
1382                && !conflict_pairs.contains(&(j, i))
1383            {
1384                conflict_pairs.push((i, j));
1385            }
1386        }
1387    }
1388
1389    let closure_param_escapes = compute_closure_param_escapes(mir);
1390
1391    FunctionBorrowSummary {
1392        param_borrows,
1393        conflict_pairs,
1394        return_summary,
1395        return_ownership_mode: super::return_ownership::infer_return_ownership_mode(
1396            mir,
1397            &HashMap::new(),
1398        ),
1399        closure_param_escapes,
1400    }
1401}
1402
1403/// Compute the per-parameter closure-escape bits (Closure Spec Phase B).
1404///
1405/// For each parameter slot, scan the function body for the §2.1 closure-escape
1406/// vectors. A parameter is marked as **escaping** (`true`) if the parameter
1407/// slot's value flows into any of:
1408///
1409///   1. The return slot (`SlotId(0)`)
1410///   2. A container store (`ArrayStore`, `ObjectStore`, `EnumStore`, `Aggregate`)
1411///   3. A struct-field write (`Assign(Place::Field{..}, Use(..))`)
1412///   4. A closure environment (`ClosureCapture.operands`)
1413///   5. A task boundary (`TaskBoundary`)
1414///   6. A deref write (`Assign(Place::Deref, Use(..))`)
1415///   7. A call-site argument (conservative — callee may store it)
1416///
1417/// If none of the vectors fire, the parameter is marked non-escaping
1418/// (`false`). Calls are conservative: we do not recursively inspect the
1419/// callee's own summary here — that would require fixed-point iteration over
1420/// the call graph, which is deferred to Phase C (monomorphization makes the
1421/// question intraprocedural). The intent of Phase B is to let callers treat
1422/// `closure_param_escapes[i] == false` as a reliable "this callee body does
1423/// not let the argument escape" bit, used by the storage planner to classify
1424/// non-escaping closures that are passed directly to known callees.
1425fn compute_closure_param_escapes(mir: &MirFunction) -> Vec<bool> {
1426    let num_params = mir.param_slots.len();
1427    let mut result = vec![true; num_params];
1428
1429    for (param_idx, &param_slot) in mir.param_slots.iter().enumerate() {
1430        result[param_idx] = param_slot_escapes(param_slot, mir);
1431    }
1432
1433    result
1434}
1435
1436/// Scan `mir` for any closure-escape vector fed by `param_slot`.
1437///
1438/// Flow propagation: starts with `{param_slot}` as the transitive-closure seed
1439/// and widens with each `Assign(Place::Local(dest), rvalue)` whose rvalue
1440/// reads a tracked slot. The scan is single-pass and monotonic (slots only
1441/// enter `tracked`, never leave), matching the §2.4 fixed-point pattern at
1442/// param-slot granularity.
1443fn param_slot_escapes(param_slot: SlotId, mir: &MirFunction) -> bool {
1444    let mut tracked: HashSet<SlotId> = HashSet::new();
1445    tracked.insert(param_slot);
1446
1447    // Iterate until no new slots are added to the tracked set. Each pass also
1448    // checks all escape vectors against the current tracked set.
1449    let mut changed = true;
1450    while changed {
1451        changed = false;
1452
1453        for block in mir.iter_blocks() {
1454            for stmt in &block.statements {
1455                match &stmt.kind {
1456                    StatementKind::Assign(place, rvalue) => {
1457                        // Direct escape: storing a tracked value into a non-Local
1458                        // destination (field / index / deref).
1459                        let reads_tracked = rvalue_uses_any(rvalue, &tracked);
1460                        match place {
1461                            Place::Local(dest) => {
1462                                if reads_tracked && tracked.insert(*dest) {
1463                                    changed = true;
1464                                }
1465                                if *dest == SlotId(0) && reads_tracked {
1466                                    return true;
1467                                }
1468                            }
1469                            Place::Field(..) | Place::Index(..) | Place::Deref(..) => {
1470                                if reads_tracked {
1471                                    return true;
1472                                }
1473                            }
1474                        }
1475
1476                        // Aggregate literal storing a tracked slot.
1477                        if let Rvalue::Aggregate(ops) = rvalue {
1478                            if ops.iter().any(|op| operand_uses_any(op, &tracked)) {
1479                                return true;
1480                            }
1481                        }
1482                    }
1483                    StatementKind::ArrayStore { operands, .. }
1484                    | StatementKind::ObjectStore { operands, .. }
1485                    | StatementKind::EnumStore { operands, .. }
1486                    | StatementKind::TaskBoundary(operands, _)
1487                    | StatementKind::ClosureCapture { operands, .. } => {
1488                        if operands.iter().any(|op| operand_uses_any(op, &tracked)) {
1489                            return true;
1490                        }
1491                    }
1492                    StatementKind::Drop(_) | StatementKind::Nop => {}
1493                }
1494            }
1495
1496            if let TerminatorKind::Call { args, .. } = &block.terminator.kind {
1497                // Conservative: a call may store any ref argument in
1498                // caller-invisible state. Phase C will refine this once the
1499                // callee's own summary is a reliable intraprocedural property.
1500                if args.iter().any(|op| operand_uses_any(op, &tracked)) {
1501                    return true;
1502                }
1503            }
1504        }
1505    }
1506
1507    false
1508}
1509
1510fn rvalue_uses_any(rvalue: &Rvalue, slots: &HashSet<SlotId>) -> bool {
1511    match rvalue {
1512        Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => {
1513            operand_uses_any(op, slots)
1514        }
1515        Rvalue::Borrow(_, place) => slots.contains(&place.root_local()),
1516        Rvalue::BinaryOp(_, lhs, rhs) => {
1517            operand_uses_any(lhs, slots) || operand_uses_any(rhs, slots)
1518        }
1519        Rvalue::Aggregate(ops) => ops.iter().any(|op| operand_uses_any(op, slots)),
1520        Rvalue::EnumTest { operand, .. }
1521        | Rvalue::EnumPayload { operand, .. }
1522        | Rvalue::TypePatternTest { operand, .. }
1523        | Rvalue::EnumDiscriminantTest { operand, .. } => operand_uses_any(operand, slots),
1524    }
1525}
1526
1527fn operand_uses_any(op: &Operand, slots: &HashSet<SlotId>) -> bool {
1528    match op {
1529        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
1530            slots.contains(&place.root_local())
1531        }
1532        Operand::Constant(_) => false,
1533    }
1534}
1535
1536/// Variant of `extract_borrow_summary` that threads a callee-summaries map so
1537/// return-mode inference can look up `Call`-returned values.
1538pub fn extract_borrow_summary_with_callees(
1539    mir: &MirFunction,
1540    return_summary: Option<ReturnReferenceSummary>,
1541    callee_modes: &HashMap<String, ReturnOwnershipMode>,
1542) -> FunctionBorrowSummary {
1543    let mut summary = extract_borrow_summary(mir, return_summary);
1544    summary.return_ownership_mode =
1545        super::return_ownership::infer_return_ownership_mode(mir, callee_modes);
1546    summary
1547}
1548
1549fn rvalue_uses_param(rvalue: &Rvalue, param_slot: SlotId) -> bool {
1550    match rvalue {
1551        Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => {
1552            operand_uses_param(op, param_slot)
1553        }
1554        Rvalue::Borrow(_, place) => place.root_local() == param_slot,
1555        Rvalue::BinaryOp(_, lhs, rhs) => {
1556            operand_uses_param(lhs, param_slot) || operand_uses_param(rhs, param_slot)
1557        }
1558        Rvalue::Aggregate(ops) => ops.iter().any(|op| operand_uses_param(op, param_slot)),
1559        Rvalue::EnumTest { operand, .. }
1560        | Rvalue::EnumPayload { operand, .. }
1561        | Rvalue::TypePatternTest { operand, .. }
1562        | Rvalue::EnumDiscriminantTest { operand, .. } => operand_uses_param(operand, param_slot),
1563    }
1564}
1565
1566fn operand_uses_param(op: &Operand, param_slot: SlotId) -> bool {
1567    match op {
1568        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
1569            place.root_local() == param_slot
1570        }
1571        Operand::Constant(_) => false,
1572    }
1573}
1574
1575pub fn analyze(mir: &MirFunction, callee_summaries: &CalleeSummaries) -> BorrowAnalysis {
1576    let cfg = ControlFlowGraph::build(mir);
1577
1578    // 1. Compute liveness (for move/clone inference)
1579    let liveness = liveness::compute_liveness(mir, &cfg);
1580
1581    // 2. Extract Datafrog input facts
1582    let facts = extract_facts(mir, &cfg, callee_summaries);
1583
1584    // 3. Run the Datafrog solver
1585    let solver_result = solve(&facts);
1586
1587    // 4. Compute ownership decisions (move/clone) based on liveness
1588    let ownership_decisions = compute_ownership_decisions(mir, &liveness);
1589    let mut move_errors = compute_use_after_move_errors(mir, &cfg, &ownership_decisions);
1590
1591    // 5. Combine into BorrowAnalysis
1592    let loans = solver_result
1593        .loan_info
1594        .into_iter()
1595        .map(|(id, info)| (LoanId(id), info))
1596        .collect();
1597    let mut errors = solver_result.errors;
1598    errors.append(&mut move_errors);
1599
1600    BorrowAnalysis {
1601        liveness,
1602        loans_at_point: solver_result.loans_at_point,
1603        loans,
1604        errors,
1605        ownership_decisions,
1606        mutability_errors: Vec::new(), // filled by binding resolver (Phase 1)
1607        return_reference_summary: solver_result.return_reference_summary,
1608    }
1609}
1610
1611/// Compute ownership decisions for assignments based on liveness.
1612fn compute_ownership_decisions(
1613    mir: &MirFunction,
1614    liveness: &LivenessResult,
1615) -> HashMap<Point, OwnershipDecision> {
1616    let mut decisions = HashMap::new();
1617
1618    for block in &mir.blocks {
1619        for (stmt_idx, stmt) in block.statements.iter().enumerate() {
1620            // Match both Move and Copy operands — identifier loads use Copy in MIR,
1621            // but the ownership decision (Move vs Clone) depends on liveness analysis.
1622            let src_slot = match &stmt.kind {
1623                StatementKind::Assign(_, Rvalue::Use(Operand::Move(Place::Local(s)))) => s,
1624                StatementKind::Assign(_, Rvalue::Use(Operand::Copy(Place::Local(s)))) => s,
1625                _ => continue,
1626            };
1627            {
1628                // Check if the source is a non-Copy type
1629                let src_type = mir
1630                    .local_types
1631                    .get(src_slot.0 as usize)
1632                    .cloned()
1633                    .unwrap_or(LocalTypeInfo::Unknown);
1634
1635                let decision = match src_type {
1636                    LocalTypeInfo::Copy => OwnershipDecision::Copy,
1637                    LocalTypeInfo::NonCopy => {
1638                        // Smart inference: check if source is live after this point
1639                        if liveness.is_live_after(block.id, stmt_idx, *src_slot, mir) {
1640                            OwnershipDecision::Clone
1641                        } else {
1642                            OwnershipDecision::Move
1643                        }
1644                    }
1645                    LocalTypeInfo::Unknown => {
1646                        // Conservative: assume Clone if live, Move if dead
1647                        if liveness.is_live_after(block.id, stmt_idx, *src_slot, mir) {
1648                            OwnershipDecision::Clone
1649                        } else {
1650                            OwnershipDecision::Move
1651                        }
1652                    }
1653                };
1654
1655                decisions.insert(stmt.point, decision);
1656            }
1657        }
1658    }
1659
1660    decisions
1661}
1662
1663fn compute_use_after_move_errors(
1664    mir: &MirFunction,
1665    cfg: &ControlFlowGraph,
1666    ownership_decisions: &HashMap<Point, OwnershipDecision>,
1667) -> Vec<BorrowError> {
1668    let mut in_states: HashMap<BasicBlockId, HashMap<Place, shape_ast::ast::Span>> = HashMap::new();
1669    let mut out_states: HashMap<BasicBlockId, HashMap<Place, shape_ast::ast::Span>> =
1670        HashMap::new();
1671
1672    for block in mir.iter_blocks() {
1673        in_states.insert(block.id, HashMap::new());
1674        out_states.insert(block.id, HashMap::new());
1675    }
1676
1677    let mut changed = true;
1678    while changed {
1679        changed = false;
1680        for &block_id in &cfg.reverse_postorder() {
1681            let mut block_in: Option<HashMap<Place, shape_ast::ast::Span>> = None;
1682            for &pred in cfg.predecessors(block_id) {
1683                if let Some(pred_out) = out_states.get(&pred) {
1684                    if let Some(current) = block_in.as_mut() {
1685                        intersect_moved_places(current, pred_out);
1686                    } else {
1687                        block_in = Some(pred_out.clone());
1688                    }
1689                }
1690            }
1691            let block_in = block_in.unwrap_or_default();
1692
1693            let mut block_out = block_in.clone();
1694            let block = mir.block(block_id);
1695            for stmt in &block.statements {
1696                apply_move_transfer(&mut block_out, stmt, mir, ownership_decisions);
1697            }
1698            // Also apply Call terminator moves (destination write clears moved status)
1699            apply_terminator_move_transfer(&mut block_out, &block.terminator);
1700
1701            if in_states.get(&block_id) != Some(&block_in) {
1702                in_states.insert(block_id, block_in);
1703                changed = true;
1704            }
1705            if out_states.get(&block_id) != Some(&block_out) {
1706                out_states.insert(block_id, block_out);
1707                changed = true;
1708            }
1709        }
1710    }
1711
1712    let mut errors = Vec::new();
1713    let mut seen = HashSet::new();
1714    for block in mir.iter_blocks() {
1715        let mut moved_places = in_states.get(&block.id).cloned().unwrap_or_default();
1716        for stmt in &block.statements {
1717            for read_place in statement_read_places(&stmt.kind) {
1718                if let Some((moved_place, move_span)) =
1719                    find_moved_place_conflict(&moved_places, &read_place)
1720                {
1721                    let key = (stmt.point.0, format!("{}", moved_place));
1722                    if seen.insert(key) {
1723                        errors.push(BorrowError {
1724                            kind: BorrowErrorKind::UseAfterMove,
1725                            span: stmt.span,
1726                            conflicting_loan: LoanId(0),
1727                            loan_span: move_span,
1728                            last_use_span: None,
1729                            repairs: Vec::new(),
1730                        });
1731                    }
1732                    break;
1733                }
1734            }
1735
1736            if let Some(borrowed_place) = statement_borrow_place(&stmt.kind)
1737                && let Some((moved_place, move_span)) =
1738                    find_moved_place_conflict(&moved_places, borrowed_place)
1739            {
1740                let key = (stmt.point.0, format!("{}", moved_place));
1741                if seen.insert(key) {
1742                    errors.push(BorrowError {
1743                        kind: BorrowErrorKind::UseAfterMove,
1744                        span: stmt.span,
1745                        conflicting_loan: LoanId(0),
1746                        loan_span: move_span,
1747                        last_use_span: None,
1748                        repairs: Vec::new(),
1749                    });
1750                }
1751            }
1752
1753            if let Some(dest_place) = statement_dest_place(&stmt.kind)
1754                && let Some((moved_place, move_span)) = moved_places
1755                    .iter()
1756                    .find(|(moved_place, _)| {
1757                        dest_place.conflicts_with(moved_place)
1758                            && !reinitializes_moved_place(dest_place, moved_place)
1759                    })
1760                    .map(|(place, span)| (place.clone(), *span))
1761            {
1762                let key = (stmt.point.0, format!("{}", moved_place));
1763                if seen.insert(key) {
1764                    errors.push(BorrowError {
1765                        kind: BorrowErrorKind::UseAfterMove,
1766                        span: stmt.span,
1767                        conflicting_loan: LoanId(0),
1768                        loan_span: move_span,
1769                        last_use_span: None,
1770                        repairs: Vec::new(),
1771                    });
1772                }
1773            }
1774
1775            apply_move_transfer(&mut moved_places, stmt, mir, ownership_decisions);
1776        }
1777
1778        // Check Call terminator for reads of moved places, then apply its transfer
1779        if let TerminatorKind::Call { func, args, destination, .. } = &block.terminator.kind {
1780            let term_key_point = block.terminator.span.start as u32;
1781            // Check func operand
1782            if let Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) = func {
1783                if let Some((moved_place, move_span)) = find_moved_place_conflict(&moved_places, place) {
1784                    let key = (term_key_point, format!("{}", moved_place));
1785                    if seen.insert(key) {
1786                        errors.push(BorrowError {
1787                            kind: BorrowErrorKind::UseAfterMove,
1788                            span: block.terminator.span,
1789                            conflicting_loan: LoanId(0),
1790                            loan_span: move_span,
1791                            last_use_span: None,
1792                            repairs: Vec::new(),
1793                        });
1794                    }
1795                }
1796            }
1797            // Check each arg
1798            for arg in args {
1799                if let Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) = arg {
1800                    if let Some((moved_place, move_span)) = find_moved_place_conflict(&moved_places, place) {
1801                        let key = (term_key_point, format!("{}", moved_place));
1802                        if seen.insert(key) {
1803                            errors.push(BorrowError {
1804                                kind: BorrowErrorKind::UseAfterMove,
1805                                span: block.terminator.span,
1806                                conflicting_loan: LoanId(0),
1807                                loan_span: move_span,
1808                                last_use_span: None,
1809                                repairs: Vec::new(),
1810                            });
1811                        }
1812                    }
1813                }
1814            }
1815            // Destination write clears moved status
1816            moved_places.retain(|moved_place, _| !reinitializes_moved_place(destination, moved_place));
1817        }
1818    }
1819
1820    errors
1821}
1822
1823fn intersect_moved_places(
1824    dest: &mut HashMap<Place, shape_ast::ast::Span>,
1825    incoming: &HashMap<Place, shape_ast::ast::Span>,
1826) {
1827    dest.retain(|place, span| {
1828        if let Some(incoming_span) = incoming.get(place) {
1829            if incoming_span.start < span.start {
1830                *span = *incoming_span;
1831            }
1832            true
1833        } else {
1834            false
1835        }
1836    });
1837}
1838
1839fn apply_move_transfer(
1840    moved_places: &mut HashMap<Place, shape_ast::ast::Span>,
1841    stmt: &MirStatement,
1842    mir: &MirFunction,
1843    ownership_decisions: &HashMap<Point, OwnershipDecision>,
1844) {
1845    if let Some(dest_place) = statement_dest_place(&stmt.kind) {
1846        moved_places.retain(|moved_place, _| !reinitializes_moved_place(dest_place, moved_place));
1847    }
1848
1849    for moved_place in actual_move_places(stmt, mir, ownership_decisions) {
1850        moved_places.insert(moved_place, stmt.span);
1851    }
1852}
1853
1854/// Apply move transfer for a Call terminator.
1855/// The call writes its return value to `destination`, which reinitializes that place.
1856/// Call args are typically temp slots created by `lower_expr_as_moved_operand` —
1857/// the moves of source values INTO those temps happen in prior statements (via Assign/Move),
1858/// not in the terminator itself, so we don't need to mark args as moved here.
1859fn apply_terminator_move_transfer(
1860    moved_places: &mut HashMap<Place, shape_ast::ast::Span>,
1861    terminator: &Terminator,
1862) {
1863    if let TerminatorKind::Call { destination, .. } = &terminator.kind {
1864        // The call writes to destination, which reinitializes that place
1865        moved_places.retain(|moved_place, _| !reinitializes_moved_place(destination, moved_place));
1866    }
1867}
1868
1869fn statement_borrow_place(kind: &StatementKind) -> Option<&Place> {
1870    match kind {
1871        StatementKind::Assign(_, Rvalue::Borrow(_, place)) => Some(place),
1872        _ => None,
1873    }
1874}
1875
1876fn statement_dest_place(kind: &StatementKind) -> Option<&Place> {
1877    match kind {
1878        StatementKind::Assign(place, _) | StatementKind::Drop(place) => Some(place),
1879        StatementKind::TaskBoundary(..)
1880        | StatementKind::ClosureCapture { .. }
1881        | StatementKind::ArrayStore { .. }
1882        | StatementKind::ObjectStore { .. }
1883        | StatementKind::EnumStore { .. } => None,
1884        StatementKind::Nop => None,
1885    }
1886}
1887
1888fn actual_move_places(
1889    stmt: &MirStatement,
1890    mir: &MirFunction,
1891    ownership_decisions: &HashMap<Point, OwnershipDecision>,
1892) -> Vec<Place> {
1893    match &stmt.kind {
1894        StatementKind::Assign(_, Rvalue::Use(Operand::Move(place)))
1895            if ownership_decisions.get(&stmt.point) == Some(&OwnershipDecision::Move) =>
1896        {
1897            vec![place.clone()]
1898        }
1899        StatementKind::Assign(_, Rvalue::Use(Operand::MoveExplicit(place)))
1900            if place_root_local_type(place, mir) != Some(LocalTypeInfo::Copy) =>
1901        {
1902            vec![place.clone()]
1903        }
1904        _ => Vec::new(),
1905    }
1906}
1907
1908fn place_root_local_type(place: &Place, mir: &MirFunction) -> Option<LocalTypeInfo> {
1909    mir.local_types.get(place.root_local().0 as usize).cloned()
1910}
1911
1912fn reinitializes_moved_place(dest_place: &Place, moved_place: &Place) -> bool {
1913    dest_place.is_prefix_of(moved_place)
1914}
1915
1916fn find_moved_place_conflict(
1917    moved_places: &HashMap<Place, shape_ast::ast::Span>,
1918    accessed_place: &Place,
1919) -> Option<(Place, shape_ast::ast::Span)> {
1920    moved_places
1921        .iter()
1922        .find(|(moved_place, _)| accessed_place.conflicts_with(moved_place))
1923        .map(|(place, span)| (place.clone(), *span))
1924}
1925
1926fn last_use_span_for_loan(facts: &BorrowFacts, loan_id: u32) -> Option<shape_ast::ast::Span> {
1927    facts
1928        .use_of_loan
1929        .iter()
1930        .filter(|(candidate, _)| *candidate == loan_id)
1931        .filter_map(|(_, point)| facts.point_spans.get(point).copied())
1932        .max_by_key(|span| span.start)
1933}
1934
1935#[cfg(test)]
1936mod tests {
1937    use super::*;
1938    use shape_ast::ast::Span;
1939
1940    fn span() -> Span {
1941        Span { start: 0, end: 1 }
1942    }
1943
1944    fn make_stmt(kind: StatementKind, point: u32) -> MirStatement {
1945        MirStatement {
1946            kind,
1947            span: span(),
1948            point: Point(point),
1949        }
1950    }
1951
1952    fn make_terminator(kind: TerminatorKind) -> Terminator {
1953        Terminator { kind, span: span() }
1954    }
1955
1956    #[test]
1957    fn test_single_shared_borrow_no_error() {
1958        let mir = MirFunction {
1959            name: "test".to_string(),
1960            blocks: vec![BasicBlock {
1961                id: BasicBlockId(0),
1962                statements: vec![
1963                    // _0 = 42
1964                    make_stmt(
1965                        StatementKind::Assign(
1966                            Place::Local(SlotId(0)),
1967                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1968                        ),
1969                        0,
1970                    ),
1971                    // _1 = &_0
1972                    make_stmt(
1973                        StatementKind::Assign(
1974                            Place::Local(SlotId(1)),
1975                            Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(0))),
1976                        ),
1977                        1,
1978                    ),
1979                ],
1980                terminator: make_terminator(TerminatorKind::Return),
1981            }],
1982            num_locals: 2,
1983            param_slots: vec![],
1984            param_reference_kinds: vec![],
1985            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::NonCopy],
1986            span: span(),
1987            field_name_table: std::collections::HashMap::new(),
1988            local_struct_type_names: std::collections::HashMap::new(),
1989            local_typed_array_element_types: std::collections::HashMap::new(),
1990            local_declared_scalar_types: std::collections::HashMap::new(),
1991        };
1992
1993        let analysis = analyze(&mir, &Default::default());
1994        assert!(analysis.errors.is_empty(), "expected no errors");
1995    }
1996
1997    #[test]
1998    fn test_conflicting_shared_and_exclusive_error() {
1999        // _0 = value
2000        // _1 = &_0 (shared)
2001        // _2 = &mut _0 (exclusive) — should conflict with _1
2002        let mir = MirFunction {
2003            name: "test".to_string(),
2004            blocks: vec![BasicBlock {
2005                id: BasicBlockId(0),
2006                statements: vec![
2007                    make_stmt(
2008                        StatementKind::Assign(
2009                            Place::Local(SlotId(0)),
2010                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2011                        ),
2012                        0,
2013                    ),
2014                    make_stmt(
2015                        StatementKind::Assign(
2016                            Place::Local(SlotId(1)),
2017                            Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(0))),
2018                        ),
2019                        1,
2020                    ),
2021                    make_stmt(
2022                        StatementKind::Assign(
2023                            Place::Local(SlotId(2)),
2024                            Rvalue::Borrow(BorrowKind::Exclusive, Place::Local(SlotId(0))),
2025                        ),
2026                        2,
2027                    ),
2028                ],
2029                terminator: make_terminator(TerminatorKind::Return),
2030            }],
2031            num_locals: 3,
2032            param_slots: vec![],
2033            param_reference_kinds: vec![],
2034            local_types: vec![
2035                LocalTypeInfo::NonCopy,
2036                LocalTypeInfo::NonCopy,
2037                LocalTypeInfo::NonCopy,
2038            ],
2039            span: span(),
2040            field_name_table: std::collections::HashMap::new(),
2041            local_struct_type_names: std::collections::HashMap::new(),
2042            local_typed_array_element_types: std::collections::HashMap::new(),
2043            local_declared_scalar_types: std::collections::HashMap::new(),
2044        };
2045
2046        let analysis = analyze(&mir, &Default::default());
2047        assert!(
2048            !analysis.errors.is_empty(),
2049            "expected borrow conflict error"
2050        );
2051        assert_eq!(
2052            analysis.errors[0].kind,
2053            BorrowErrorKind::ConflictSharedExclusive
2054        );
2055    }
2056
2057    #[test]
2058    fn test_disjoint_field_borrows_no_conflict() {
2059        // _1 = &_0.a (shared)
2060        // _2 = &mut _0.b (exclusive) — disjoint fields, no conflict
2061        let mir = MirFunction {
2062            name: "test".to_string(),
2063            blocks: vec![BasicBlock {
2064                id: BasicBlockId(0),
2065                statements: vec![
2066                    make_stmt(
2067                        StatementKind::Assign(
2068                            Place::Local(SlotId(0)),
2069                            Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
2070                        ),
2071                        0,
2072                    ),
2073                    make_stmt(
2074                        StatementKind::Assign(
2075                            Place::Local(SlotId(1)),
2076                            Rvalue::Borrow(
2077                                BorrowKind::Shared,
2078                                Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(0)),
2079                            ),
2080                        ),
2081                        1,
2082                    ),
2083                    make_stmt(
2084                        StatementKind::Assign(
2085                            Place::Local(SlotId(2)),
2086                            Rvalue::Borrow(
2087                                BorrowKind::Exclusive,
2088                                Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(1)),
2089                            ),
2090                        ),
2091                        2,
2092                    ),
2093                ],
2094                terminator: make_terminator(TerminatorKind::Return),
2095            }],
2096            num_locals: 3,
2097            param_slots: vec![],
2098            param_reference_kinds: vec![],
2099            local_types: vec![
2100                LocalTypeInfo::NonCopy,
2101                LocalTypeInfo::NonCopy,
2102                LocalTypeInfo::NonCopy,
2103            ],
2104            span: span(),
2105            field_name_table: std::collections::HashMap::new(),
2106            local_struct_type_names: std::collections::HashMap::new(),
2107            local_typed_array_element_types: std::collections::HashMap::new(),
2108            local_declared_scalar_types: std::collections::HashMap::new(),
2109        };
2110
2111        let analysis = analyze(&mir, &Default::default());
2112        assert!(
2113            analysis.errors.is_empty(),
2114            "disjoint field borrows should not conflict, got: {:?}",
2115            analysis.errors
2116        );
2117    }
2118
2119    #[test]
2120    fn test_read_while_exclusive_borrow_error() {
2121        let mir = MirFunction {
2122            name: "test".to_string(),
2123            blocks: vec![BasicBlock {
2124                id: BasicBlockId(0),
2125                statements: vec![
2126                    make_stmt(
2127                        StatementKind::Assign(
2128                            Place::Local(SlotId(0)),
2129                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2130                        ),
2131                        0,
2132                    ),
2133                    make_stmt(
2134                        StatementKind::Assign(
2135                            Place::Local(SlotId(1)),
2136                            Rvalue::Borrow(BorrowKind::Exclusive, Place::Local(SlotId(0))),
2137                        ),
2138                        1,
2139                    ),
2140                    make_stmt(
2141                        StatementKind::Assign(
2142                            Place::Local(SlotId(2)),
2143                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
2144                        ),
2145                        2,
2146                    ),
2147                ],
2148                terminator: make_terminator(TerminatorKind::Return),
2149            }],
2150            num_locals: 3,
2151            param_slots: vec![],
2152            param_reference_kinds: vec![],
2153            local_types: vec![
2154                LocalTypeInfo::NonCopy,
2155                LocalTypeInfo::NonCopy,
2156                LocalTypeInfo::NonCopy,
2157            ],
2158            span: span(),
2159            field_name_table: std::collections::HashMap::new(),
2160            local_struct_type_names: std::collections::HashMap::new(),
2161            local_typed_array_element_types: std::collections::HashMap::new(),
2162            local_declared_scalar_types: std::collections::HashMap::new(),
2163        };
2164
2165        let analysis = analyze(&mir, &Default::default());
2166        assert!(
2167            analysis
2168                .errors
2169                .iter()
2170                .any(|error| error.kind == BorrowErrorKind::ReadWhileExclusivelyBorrowed),
2171            "expected read-while-exclusive error, got {:?}",
2172            analysis.errors
2173        );
2174    }
2175
2176    #[test]
2177    fn test_reference_escape_error_for_returned_ref_alias() {
2178        let mir = MirFunction {
2179            name: "test".to_string(),
2180            blocks: vec![BasicBlock {
2181                id: BasicBlockId(0),
2182                statements: vec![
2183                    make_stmt(
2184                        StatementKind::Assign(
2185                            Place::Local(SlotId(1)),
2186                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2187                        ),
2188                        0,
2189                    ),
2190                    make_stmt(
2191                        StatementKind::Assign(
2192                            Place::Local(SlotId(2)),
2193                            Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(1))),
2194                        ),
2195                        1,
2196                    ),
2197                    make_stmt(
2198                        StatementKind::Assign(
2199                            Place::Local(SlotId(3)),
2200                            Rvalue::Use(Operand::Move(Place::Local(SlotId(2)))),
2201                        ),
2202                        2,
2203                    ),
2204                    make_stmt(
2205                        StatementKind::Assign(
2206                            Place::Local(SlotId(0)),
2207                            Rvalue::Use(Operand::Move(Place::Local(SlotId(3)))),
2208                        ),
2209                        3,
2210                    ),
2211                ],
2212                terminator: make_terminator(TerminatorKind::Return),
2213            }],
2214            num_locals: 4,
2215            param_slots: vec![],
2216            param_reference_kinds: vec![],
2217            local_types: vec![
2218                LocalTypeInfo::NonCopy,
2219                LocalTypeInfo::NonCopy,
2220                LocalTypeInfo::NonCopy,
2221                LocalTypeInfo::NonCopy,
2222            ],
2223            span: span(),
2224            field_name_table: std::collections::HashMap::new(),
2225            local_struct_type_names: std::collections::HashMap::new(),
2226            local_typed_array_element_types: std::collections::HashMap::new(),
2227            local_declared_scalar_types: std::collections::HashMap::new(),
2228        };
2229
2230        let analysis = analyze(&mir, &Default::default());
2231        assert!(
2232            analysis
2233                .errors
2234                .iter()
2235                .any(|error| error.kind == BorrowErrorKind::ReferenceEscape),
2236            "expected reference-escape error, got {:?}",
2237            analysis.errors
2238        );
2239    }
2240
2241    #[test]
2242    fn test_use_after_explicit_move_error() {
2243        let mir = MirFunction {
2244            name: "test".to_string(),
2245            blocks: vec![BasicBlock {
2246                id: BasicBlockId(0),
2247                statements: vec![
2248                    make_stmt(
2249                        StatementKind::Assign(
2250                            Place::Local(SlotId(0)),
2251                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2252                        ),
2253                        0,
2254                    ),
2255                    make_stmt(
2256                        StatementKind::Assign(
2257                            Place::Local(SlotId(1)),
2258                            Rvalue::Use(Operand::MoveExplicit(Place::Local(SlotId(0)))),
2259                        ),
2260                        1,
2261                    ),
2262                    make_stmt(
2263                        StatementKind::Assign(
2264                            Place::Local(SlotId(2)),
2265                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
2266                        ),
2267                        2,
2268                    ),
2269                ],
2270                terminator: make_terminator(TerminatorKind::Return),
2271            }],
2272            num_locals: 3,
2273            param_slots: vec![],
2274            param_reference_kinds: vec![],
2275            local_types: vec![
2276                LocalTypeInfo::NonCopy,
2277                LocalTypeInfo::NonCopy,
2278                LocalTypeInfo::NonCopy,
2279            ],
2280            span: span(),
2281            field_name_table: std::collections::HashMap::new(),
2282            local_struct_type_names: std::collections::HashMap::new(),
2283            local_typed_array_element_types: std::collections::HashMap::new(),
2284            local_declared_scalar_types: std::collections::HashMap::new(),
2285        };
2286
2287        let analysis = analyze(&mir, &Default::default());
2288        assert!(
2289            analysis
2290                .errors
2291                .iter()
2292                .any(|error| error.kind == BorrowErrorKind::UseAfterMove),
2293            "expected use-after-move error, got {:?}",
2294            analysis.errors
2295        );
2296    }
2297
2298    #[test]
2299    fn test_move_vs_clone_decision() {
2300        // _0 = value (NonCopy)
2301        // _1 = move _0  (point 1 — _0 NOT live after → Move)
2302        let mir = MirFunction {
2303            name: "test".to_string(),
2304            blocks: vec![BasicBlock {
2305                id: BasicBlockId(0),
2306                statements: vec![
2307                    make_stmt(
2308                        StatementKind::Assign(
2309                            Place::Local(SlotId(0)),
2310                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2311                        ),
2312                        0,
2313                    ),
2314                    make_stmt(
2315                        StatementKind::Assign(
2316                            Place::Local(SlotId(1)),
2317                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2318                        ),
2319                        1,
2320                    ),
2321                ],
2322                terminator: make_terminator(TerminatorKind::Return),
2323            }],
2324            num_locals: 2,
2325            param_slots: vec![],
2326            param_reference_kinds: vec![],
2327            local_types: vec![LocalTypeInfo::NonCopy, LocalTypeInfo::NonCopy],
2328            span: span(),
2329            field_name_table: std::collections::HashMap::new(),
2330            local_struct_type_names: std::collections::HashMap::new(),
2331            local_typed_array_element_types: std::collections::HashMap::new(),
2332            local_declared_scalar_types: std::collections::HashMap::new(),
2333        };
2334
2335        let analysis = analyze(&mir, &Default::default());
2336        // _0 is not used after point 1, so decision should be Move
2337        assert_eq!(
2338            analysis.ownership_at(Point(1)),
2339            OwnershipDecision::Move,
2340            "source dead after → should be Move"
2341        );
2342    }
2343
2344    #[test]
2345    fn test_nll_borrow_scoping() {
2346        // NLL test: borrow ends at last use, not at lexical scope exit
2347        // bb0: _0 = value; _1 = &_0; (use _1 here); goto bb1
2348        // bb1: _2 = &mut _0 — should be OK because _1 is no longer used
2349        let mir = MirFunction {
2350            name: "test".to_string(),
2351            blocks: vec![
2352                BasicBlock {
2353                    id: BasicBlockId(0),
2354                    statements: vec![
2355                        make_stmt(
2356                            StatementKind::Assign(
2357                                Place::Local(SlotId(0)),
2358                                Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2359                            ),
2360                            0,
2361                        ),
2362                        make_stmt(
2363                            StatementKind::Assign(
2364                                Place::Local(SlotId(1)),
2365                                Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(0))),
2366                            ),
2367                            1,
2368                        ),
2369                        // Use _1
2370                        make_stmt(
2371                            StatementKind::Assign(
2372                                Place::Local(SlotId(3)),
2373                                Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2374                            ),
2375                            2,
2376                        ),
2377                    ],
2378                    terminator: make_terminator(TerminatorKind::Goto(BasicBlockId(1))),
2379                },
2380                BasicBlock {
2381                    id: BasicBlockId(1),
2382                    statements: vec![
2383                        // _1 is no longer used here — shared borrow should be "dead"
2384                        // So taking &mut _0 should be OK
2385                        make_stmt(
2386                            StatementKind::Assign(
2387                                Place::Local(SlotId(2)),
2388                                Rvalue::Borrow(BorrowKind::Exclusive, Place::Local(SlotId(0))),
2389                            ),
2390                            3,
2391                        ),
2392                    ],
2393                    terminator: make_terminator(TerminatorKind::Return),
2394                },
2395            ],
2396            num_locals: 4,
2397            param_slots: vec![],
2398            param_reference_kinds: vec![],
2399            local_types: vec![
2400                LocalTypeInfo::NonCopy,
2401                LocalTypeInfo::NonCopy,
2402                LocalTypeInfo::NonCopy,
2403                LocalTypeInfo::NonCopy,
2404            ],
2405            span: span(),
2406            field_name_table: std::collections::HashMap::new(),
2407            local_struct_type_names: std::collections::HashMap::new(),
2408            local_typed_array_element_types: std::collections::HashMap::new(),
2409            local_declared_scalar_types: std::collections::HashMap::new(),
2410        };
2411
2412        let analysis = analyze(&mir, &Default::default());
2413        // With NLL, the shared borrow on _0 ends after last use of _1 (point 2).
2414        // The exclusive borrow at point 3 should NOT conflict.
2415        // Note: our current solver propagates loan_live_at through cfg_edge
2416        // without checking if the loan is actually used. For full NLL we need
2417        // to intersect with "loan_used_at" — this is tracked as a known TODO.
2418        // For now, this test documents the current behavior.
2419        let _ = analysis;
2420    }
2421
2422    #[test]
2423    fn test_clone_decision_when_source_live_after() {
2424        // _0 = value (NonCopy)
2425        // _1 = move _0 (point 1 — _0 IS live after because _2 uses it)
2426        // _2 = move _0 (point 2 — _0 NOT live after → Move)
2427        let mir = MirFunction {
2428            name: "test".to_string(),
2429            blocks: vec![BasicBlock {
2430                id: BasicBlockId(0),
2431                statements: vec![
2432                    make_stmt(
2433                        StatementKind::Assign(
2434                            Place::Local(SlotId(0)),
2435                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2436                        ),
2437                        0,
2438                    ),
2439                    make_stmt(
2440                        StatementKind::Assign(
2441                            Place::Local(SlotId(1)),
2442                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2443                        ),
2444                        1,
2445                    ),
2446                    make_stmt(
2447                        StatementKind::Assign(
2448                            Place::Local(SlotId(2)),
2449                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2450                        ),
2451                        2,
2452                    ),
2453                ],
2454                terminator: make_terminator(TerminatorKind::Return),
2455            }],
2456            num_locals: 3,
2457            param_slots: vec![],
2458            param_reference_kinds: vec![],
2459            local_types: vec![
2460                LocalTypeInfo::NonCopy,
2461                LocalTypeInfo::NonCopy,
2462                LocalTypeInfo::NonCopy,
2463            ],
2464            span: span(),
2465            field_name_table: std::collections::HashMap::new(),
2466            local_struct_type_names: std::collections::HashMap::new(),
2467            local_typed_array_element_types: std::collections::HashMap::new(),
2468            local_declared_scalar_types: std::collections::HashMap::new(),
2469        };
2470
2471        let analysis = analyze(&mir, &Default::default());
2472        // At point 1, _0 is still used at point 2, so it's live → Clone
2473        assert_eq!(
2474            analysis.ownership_at(Point(1)),
2475            OwnershipDecision::Clone,
2476            "source live after → should be Clone"
2477        );
2478        // At point 2, _0 is not used after → Move
2479        assert_eq!(
2480            analysis.ownership_at(Point(2)),
2481            OwnershipDecision::Move,
2482            "source dead after → should be Move"
2483        );
2484    }
2485
2486    #[test]
2487    fn test_copy_type_always_copy_decision() {
2488        // _0 = 42 (Copy type)
2489        // _1 = move _0 — but since _0 is Copy, decision should be Copy
2490        let mir = MirFunction {
2491            name: "test".to_string(),
2492            blocks: vec![BasicBlock {
2493                id: BasicBlockId(0),
2494                statements: vec![
2495                    make_stmt(
2496                        StatementKind::Assign(
2497                            Place::Local(SlotId(0)),
2498                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2499                        ),
2500                        0,
2501                    ),
2502                    make_stmt(
2503                        StatementKind::Assign(
2504                            Place::Local(SlotId(1)),
2505                            Rvalue::Use(Operand::Move(Place::Local(SlotId(0)))),
2506                        ),
2507                        1,
2508                    ),
2509                ],
2510                terminator: make_terminator(TerminatorKind::Return),
2511            }],
2512            num_locals: 2,
2513            param_slots: vec![],
2514            param_reference_kinds: vec![],
2515            local_types: vec![LocalTypeInfo::Copy, LocalTypeInfo::Copy],
2516            span: span(),
2517            field_name_table: std::collections::HashMap::new(),
2518            local_struct_type_names: std::collections::HashMap::new(),
2519            local_typed_array_element_types: std::collections::HashMap::new(),
2520            local_declared_scalar_types: std::collections::HashMap::new(),
2521        };
2522
2523        let analysis = analyze(&mir, &Default::default());
2524        assert_eq!(
2525            analysis.ownership_at(Point(1)),
2526            OwnershipDecision::Copy,
2527            "Copy type → always Copy regardless of liveness"
2528        );
2529    }
2530
2531    // =========================================================================
2532    // compose_return_reference_summary unit tests
2533    // =========================================================================
2534
2535    #[test]
2536    fn test_compose_summary_identity() {
2537        // Both empty projections — identity composition
2538        let arg = ReturnReferenceSummary {
2539            param_index: 2,
2540            kind: BorrowKind::Shared,
2541            projection: Some(vec![]),
2542        };
2543        let callee = ReturnReferenceSummary {
2544            param_index: 0,
2545            kind: BorrowKind::Exclusive,
2546            projection: Some(vec![]),
2547        };
2548        let result = compose_return_reference_summary(&arg, &callee);
2549        assert_eq!(result.param_index, 2); // from arg
2550        assert_eq!(result.kind, BorrowKind::Exclusive); // from callee
2551        assert_eq!(result.projection, Some(vec![]));
2552    }
2553
2554    #[test]
2555    fn test_compose_summary_some_index_some_empty() {
2556        let arg = ReturnReferenceSummary {
2557            param_index: 0,
2558            kind: BorrowKind::Shared,
2559            projection: Some(vec![ProjectionStep::Index]),
2560        };
2561        let callee = ReturnReferenceSummary {
2562            param_index: 0,
2563            kind: BorrowKind::Shared,
2564            projection: Some(vec![]),
2565        };
2566        let result = compose_return_reference_summary(&arg, &callee);
2567        assert_eq!(result.projection, Some(vec![ProjectionStep::Index]));
2568    }
2569
2570    #[test]
2571    fn test_compose_summary_callee_field_loses_precision() {
2572        let arg = ReturnReferenceSummary {
2573            param_index: 0,
2574            kind: BorrowKind::Shared,
2575            projection: Some(vec![]),
2576        };
2577        let callee = ReturnReferenceSummary {
2578            param_index: 0,
2579            kind: BorrowKind::Shared,
2580            projection: Some(vec![ProjectionStep::Field(FieldIdx(0))]),
2581        };
2582        let result = compose_return_reference_summary(&arg, &callee);
2583        assert_eq!(result.projection, None); // Field loses precision
2584    }
2585
2586    #[test]
2587    fn test_compose_summary_callee_index_composes() {
2588        let arg = ReturnReferenceSummary {
2589            param_index: 1,
2590            kind: BorrowKind::Shared,
2591            projection: Some(vec![ProjectionStep::Index]),
2592        };
2593        let callee = ReturnReferenceSummary {
2594            param_index: 0,
2595            kind: BorrowKind::Exclusive,
2596            projection: Some(vec![ProjectionStep::Index]),
2597        };
2598        let result = compose_return_reference_summary(&arg, &callee);
2599        assert_eq!(result.param_index, 1);
2600        assert_eq!(result.kind, BorrowKind::Exclusive);
2601        assert_eq!(
2602            result.projection,
2603            Some(vec![ProjectionStep::Index, ProjectionStep::Index])
2604        );
2605    }
2606
2607    #[test]
2608    fn test_compose_summary_arg_none() {
2609        let arg = ReturnReferenceSummary {
2610            param_index: 0,
2611            kind: BorrowKind::Shared,
2612            projection: None, // precision already lost
2613        };
2614        let callee = ReturnReferenceSummary {
2615            param_index: 0,
2616            kind: BorrowKind::Shared,
2617            projection: Some(vec![]),
2618        };
2619        let result = compose_return_reference_summary(&arg, &callee);
2620        assert_eq!(result.projection, None);
2621    }
2622
2623    #[test]
2624    fn test_compose_summary_callee_none() {
2625        let arg = ReturnReferenceSummary {
2626            param_index: 0,
2627            kind: BorrowKind::Shared,
2628            projection: Some(vec![ProjectionStep::Index]),
2629        };
2630        let callee = ReturnReferenceSummary {
2631            param_index: 0,
2632            kind: BorrowKind::Exclusive,
2633            projection: None,
2634        };
2635        let result = compose_return_reference_summary(&arg, &callee);
2636        assert_eq!(result.projection, None);
2637    }
2638
2639    // =========================================================================
2640    // Solver-level call composition tests (synthetic MIR)
2641    // =========================================================================
2642
2643    #[test]
2644    fn test_call_composition_identity() {
2645        // fn identity(&x) { x }
2646        // Caller: param _1 (&ref), call identity(_1) → _2, return _2
2647        // With callee summary for "identity": param_index=0, kind=Shared, projection=Some([])
2648        let mir = MirFunction {
2649            name: "caller".to_string(),
2650            blocks: vec![
2651                BasicBlock {
2652                    id: BasicBlockId(0),
2653                    statements: vec![
2654                        MirStatement {
2655                            kind: StatementKind::Assign(
2656                                Place::Local(SlotId(2)),
2657                                Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2658                            ),
2659                            span: span(),
2660                            point: Point(0),
2661                        },
2662                    ],
2663                    terminator: Terminator {
2664                        kind: TerminatorKind::Call {
2665                            func: Operand::Constant(MirConstant::Function(
2666                                "identity".to_string(),
2667                            )),
2668                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2669                            destination: Place::Local(SlotId(3)),
2670                            next: BasicBlockId(1),
2671                        },
2672                        span: span(),
2673                    },
2674                },
2675                BasicBlock {
2676                    id: BasicBlockId(1),
2677                    statements: vec![MirStatement {
2678                        kind: StatementKind::Assign(
2679                            Place::Local(SlotId(0)),
2680                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(3)))),
2681                        ),
2682                        span: span(),
2683                        point: Point(1),
2684                    }],
2685                    terminator: Terminator {
2686                        kind: TerminatorKind::Return,
2687                        span: span(),
2688                    },
2689                },
2690            ],
2691            num_locals: 4,
2692            param_slots: vec![SlotId(1)],
2693            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2694            local_types: vec![
2695                LocalTypeInfo::NonCopy,
2696                LocalTypeInfo::NonCopy,
2697                LocalTypeInfo::NonCopy,
2698                LocalTypeInfo::NonCopy,
2699            ],
2700            span: span(),
2701            field_name_table: std::collections::HashMap::new(),
2702            local_struct_type_names: std::collections::HashMap::new(),
2703            local_typed_array_element_types: std::collections::HashMap::new(),
2704            local_declared_scalar_types: std::collections::HashMap::new(),
2705        };
2706
2707        let mut callee_summaries = CalleeSummaries::new();
2708        callee_summaries.insert(
2709            "identity".to_string(),
2710            ReturnReferenceSummary {
2711                param_index: 0,
2712                kind: BorrowKind::Shared,
2713                projection: Some(vec![]),
2714            },
2715        );
2716
2717        let analysis = analyze(&mir, &callee_summaries);
2718        assert!(
2719            analysis.return_reference_summary.is_some(),
2720            "expected return reference summary from composed call"
2721        );
2722        let summary = analysis.return_reference_summary.unwrap();
2723        assert_eq!(summary.param_index, 0);
2724        assert_eq!(summary.kind, BorrowKind::Shared);
2725    }
2726
2727    #[test]
2728    fn test_call_composition_unknown_callee() {
2729        // Same as above but no callee summary → conservative (no return summary)
2730        let mir = MirFunction {
2731            name: "caller".to_string(),
2732            blocks: vec![
2733                BasicBlock {
2734                    id: BasicBlockId(0),
2735                    statements: vec![MirStatement {
2736                        kind: StatementKind::Assign(
2737                            Place::Local(SlotId(2)),
2738                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2739                        ),
2740                        span: span(),
2741                        point: Point(0),
2742                    }],
2743                    terminator: Terminator {
2744                        kind: TerminatorKind::Call {
2745                            func: Operand::Constant(MirConstant::Function(
2746                                "unknown_fn".to_string(),
2747                            )),
2748                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2749                            destination: Place::Local(SlotId(3)),
2750                            next: BasicBlockId(1),
2751                        },
2752                        span: span(),
2753                    },
2754                },
2755                BasicBlock {
2756                    id: BasicBlockId(1),
2757                    statements: vec![MirStatement {
2758                        kind: StatementKind::Assign(
2759                            Place::Local(SlotId(0)),
2760                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(3)))),
2761                        ),
2762                        span: span(),
2763                        point: Point(1),
2764                    }],
2765                    terminator: Terminator {
2766                        kind: TerminatorKind::Return,
2767                        span: span(),
2768                    },
2769                },
2770            ],
2771            num_locals: 4,
2772            param_slots: vec![SlotId(1)],
2773            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2774            local_types: vec![
2775                LocalTypeInfo::NonCopy,
2776                LocalTypeInfo::NonCopy,
2777                LocalTypeInfo::NonCopy,
2778                LocalTypeInfo::NonCopy,
2779            ],
2780            span: span(),
2781            field_name_table: std::collections::HashMap::new(),
2782            local_struct_type_names: std::collections::HashMap::new(),
2783            local_typed_array_element_types: std::collections::HashMap::new(),
2784            local_declared_scalar_types: std::collections::HashMap::new(),
2785        };
2786
2787        let analysis = analyze(&mir, &Default::default());
2788        // Unknown callee → no return reference summary composed
2789        assert!(
2790            analysis.return_reference_summary.is_none(),
2791            "unknown callee should not produce return reference summary"
2792        );
2793    }
2794
2795    #[test]
2796    fn test_call_composition_indirect_call() {
2797        // Call via Method (not Function) → conservative
2798        let mir = MirFunction {
2799            name: "caller".to_string(),
2800            blocks: vec![
2801                BasicBlock {
2802                    id: BasicBlockId(0),
2803                    statements: vec![MirStatement {
2804                        kind: StatementKind::Assign(
2805                            Place::Local(SlotId(2)),
2806                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2807                        ),
2808                        span: span(),
2809                        point: Point(0),
2810                    }],
2811                    terminator: Terminator {
2812                        kind: TerminatorKind::Call {
2813                            func: Operand::Constant(MirConstant::Method(
2814                                "identity".to_string(),
2815                            )),
2816                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2817                            destination: Place::Local(SlotId(3)),
2818                            next: BasicBlockId(1),
2819                        },
2820                        span: span(),
2821                    },
2822                },
2823                BasicBlock {
2824                    id: BasicBlockId(1),
2825                    statements: vec![MirStatement {
2826                        kind: StatementKind::Assign(
2827                            Place::Local(SlotId(0)),
2828                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(3)))),
2829                        ),
2830                        span: span(),
2831                        point: Point(1),
2832                    }],
2833                    terminator: Terminator {
2834                        kind: TerminatorKind::Return,
2835                        span: span(),
2836                    },
2837                },
2838            ],
2839            num_locals: 4,
2840            param_slots: vec![SlotId(1)],
2841            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2842            local_types: vec![
2843                LocalTypeInfo::NonCopy,
2844                LocalTypeInfo::NonCopy,
2845                LocalTypeInfo::NonCopy,
2846                LocalTypeInfo::NonCopy,
2847            ],
2848            span: span(),
2849            field_name_table: std::collections::HashMap::new(),
2850            local_struct_type_names: std::collections::HashMap::new(),
2851            local_typed_array_element_types: std::collections::HashMap::new(),
2852            local_declared_scalar_types: std::collections::HashMap::new(),
2853        };
2854
2855        let mut callee_summaries = CalleeSummaries::new();
2856        callee_summaries.insert(
2857            "identity".to_string(),
2858            ReturnReferenceSummary {
2859                param_index: 0,
2860                kind: BorrowKind::Shared,
2861                projection: Some(vec![]),
2862            },
2863        );
2864
2865        // Method call, not Function call → conservative even with summary present
2866        let analysis = analyze(&mir, &callee_summaries);
2867        assert!(
2868            analysis.return_reference_summary.is_none(),
2869            "indirect (Method) call should not compose return summary"
2870        );
2871    }
2872
2873    #[test]
2874    fn test_call_composition_chain() {
2875        // Two-deep: param _1 → call "inner"(_1) → _3, call "outer"(_3) → _4, return _4
2876        // inner: param_index=0, kind=Shared, projection=Some([])
2877        // outer: param_index=0, kind=Exclusive, projection=Some([])
2878        // Result: param_index=0 (traces to caller's param), kind=Exclusive (outer dictates)
2879        let mir = MirFunction {
2880            name: "caller".to_string(),
2881            blocks: vec![
2882                BasicBlock {
2883                    id: BasicBlockId(0),
2884                    statements: vec![MirStatement {
2885                        kind: StatementKind::Assign(
2886                            Place::Local(SlotId(2)),
2887                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2888                        ),
2889                        span: span(),
2890                        point: Point(0),
2891                    }],
2892                    terminator: Terminator {
2893                        kind: TerminatorKind::Call {
2894                            func: Operand::Constant(MirConstant::Function(
2895                                "inner".to_string(),
2896                            )),
2897                            args: vec![Operand::Copy(Place::Local(SlotId(1)))],
2898                            destination: Place::Local(SlotId(3)),
2899                            next: BasicBlockId(1),
2900                        },
2901                        span: span(),
2902                    },
2903                },
2904                BasicBlock {
2905                    id: BasicBlockId(1),
2906                    statements: vec![MirStatement {
2907                        kind: StatementKind::Nop,
2908                        span: span(),
2909                        point: Point(1),
2910                    }],
2911                    terminator: Terminator {
2912                        kind: TerminatorKind::Call {
2913                            func: Operand::Constant(MirConstant::Function(
2914                                "outer".to_string(),
2915                            )),
2916                            args: vec![Operand::Copy(Place::Local(SlotId(3)))],
2917                            destination: Place::Local(SlotId(4)),
2918                            next: BasicBlockId(2),
2919                        },
2920                        span: span(),
2921                    },
2922                },
2923                BasicBlock {
2924                    id: BasicBlockId(2),
2925                    statements: vec![MirStatement {
2926                        kind: StatementKind::Assign(
2927                            Place::Local(SlotId(0)),
2928                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(4)))),
2929                        ),
2930                        span: span(),
2931                        point: Point(2),
2932                    }],
2933                    terminator: Terminator {
2934                        kind: TerminatorKind::Return,
2935                        span: span(),
2936                    },
2937                },
2938            ],
2939            num_locals: 5,
2940            param_slots: vec![SlotId(1)],
2941            param_reference_kinds: vec![Some(BorrowKind::Shared)],
2942            local_types: vec![
2943                LocalTypeInfo::NonCopy,
2944                LocalTypeInfo::NonCopy,
2945                LocalTypeInfo::NonCopy,
2946                LocalTypeInfo::NonCopy,
2947                LocalTypeInfo::NonCopy,
2948            ],
2949            span: span(),
2950            field_name_table: std::collections::HashMap::new(),
2951            local_struct_type_names: std::collections::HashMap::new(),
2952            local_typed_array_element_types: std::collections::HashMap::new(),
2953            local_declared_scalar_types: std::collections::HashMap::new(),
2954        };
2955
2956        let mut callee_summaries = CalleeSummaries::new();
2957        callee_summaries.insert(
2958            "inner".to_string(),
2959            ReturnReferenceSummary {
2960                param_index: 0,
2961                kind: BorrowKind::Shared,
2962                projection: Some(vec![]),
2963            },
2964        );
2965        callee_summaries.insert(
2966            "outer".to_string(),
2967            ReturnReferenceSummary {
2968                param_index: 0,
2969                kind: BorrowKind::Exclusive,
2970                projection: Some(vec![]),
2971            },
2972        );
2973
2974        let analysis = analyze(&mir, &callee_summaries);
2975        assert!(
2976            analysis.return_reference_summary.is_some(),
2977            "chained composition should produce return reference summary"
2978        );
2979        let summary = analysis.return_reference_summary.unwrap();
2980        assert_eq!(summary.param_index, 0, "should trace to outermost param");
2981        assert_eq!(
2982            summary.kind,
2983            BorrowKind::Exclusive,
2984            "outer callee dictates the kind"
2985        );
2986    }
2987
2988    // ── Closure Spec Phase B: closure_param_escapes extraction ──────────
2989
2990    /// Build a minimal MIR function with the given parameter count and body.
2991    fn make_param_mir(
2992        name: &str,
2993        num_params: u16,
2994        body: Vec<MirStatement>,
2995        num_locals: u16,
2996    ) -> MirFunction {
2997        // Params occupy slots starting at 1 (slot 0 is the return).
2998        let param_slots: Vec<SlotId> = (1..=num_params).map(SlotId).collect();
2999        MirFunction {
3000            name: name.to_string(),
3001            blocks: vec![BasicBlock {
3002                id: BasicBlockId(0),
3003                statements: body,
3004                terminator: make_terminator(TerminatorKind::Return),
3005            }],
3006            num_locals,
3007            param_slots,
3008            param_reference_kinds: vec![None; num_params as usize],
3009            local_types: vec![LocalTypeInfo::Unknown; num_locals as usize],
3010            span: span(),
3011            field_name_table: std::collections::HashMap::new(),
3012            local_struct_type_names: std::collections::HashMap::new(),
3013            local_typed_array_element_types: std::collections::HashMap::new(),
3014            local_declared_scalar_types: std::collections::HashMap::new(),
3015        }
3016    }
3017
3018    #[test]
3019    fn test_closure_param_escapes_pure_body_marks_non_escaping() {
3020        // fn f(p) { let _ = p + 1 }  — p is read but never escapes.
3021        // Slots: _0 = return, _1 = p, _2 = tmp
3022        let mir = make_param_mir(
3023            "pure_param",
3024            1,
3025            vec![make_stmt(
3026                StatementKind::Assign(
3027                    Place::Local(SlotId(2)),
3028                    Rvalue::BinaryOp(
3029                        BinOp::Add,
3030                        Operand::Copy(Place::Local(SlotId(1))),
3031                        Operand::Constant(MirConstant::Int(1)),
3032                    ),
3033                ),
3034                0,
3035            )],
3036            3,
3037        );
3038
3039        let summary = extract_borrow_summary(&mir, None);
3040        assert_eq!(
3041            summary.closure_param_escapes,
3042            vec![false],
3043            "pure arithmetic body does not escape the param"
3044        );
3045    }
3046
3047    #[test]
3048    fn test_closure_param_escapes_return_marks_escaping() {
3049        // fn f(p) { p } — p flows to the return slot.
3050        let mir = make_param_mir(
3051            "return_param",
3052            1,
3053            vec![make_stmt(
3054                StatementKind::Assign(
3055                    Place::Local(SlotId(0)),
3056                    Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
3057                ),
3058                0,
3059            )],
3060            2,
3061        );
3062
3063        let summary = extract_borrow_summary(&mir, None);
3064        assert_eq!(
3065            summary.closure_param_escapes,
3066            vec![true],
3067            "param returned from function must be marked as escaping"
3068        );
3069    }
3070
3071    #[test]
3072    fn test_closure_param_escapes_captured_marks_escaping() {
3073        // fn f(p) { || p }  — p captured into a closure.
3074        let mir = make_param_mir(
3075            "capture_param",
3076            1,
3077            vec![make_stmt(
3078                StatementKind::ClosureCapture {
3079                    closure_slot: SlotId(2),
3080                    operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
3081                    function_id: None,
3082                },
3083                0,
3084            )],
3085            3,
3086        );
3087
3088        let summary = extract_borrow_summary(&mir, None);
3089        assert_eq!(
3090            summary.closure_param_escapes,
3091            vec![true],
3092            "param captured into a closure must be marked as escaping"
3093        );
3094    }
3095
3096    #[test]
3097    fn test_closure_param_escapes_mixed_params() {
3098        // fn f(a, b) { let _ = a + 1; return b }
3099        //   a is pure, b is returned.
3100        let mir = make_param_mir(
3101            "mixed_params",
3102            2,
3103            vec![
3104                make_stmt(
3105                    StatementKind::Assign(
3106                        Place::Local(SlotId(3)),
3107                        Rvalue::BinaryOp(
3108                            BinOp::Add,
3109                            Operand::Copy(Place::Local(SlotId(1))),
3110                            Operand::Constant(MirConstant::Int(1)),
3111                        ),
3112                    ),
3113                    0,
3114                ),
3115                make_stmt(
3116                    StatementKind::Assign(
3117                        Place::Local(SlotId(0)),
3118                        Rvalue::Use(Operand::Copy(Place::Local(SlotId(2)))),
3119                    ),
3120                    1,
3121                ),
3122            ],
3123            4,
3124        );
3125
3126        let summary = extract_borrow_summary(&mir, None);
3127        assert_eq!(
3128            summary.closure_param_escapes,
3129            vec![false, true],
3130            "a non-escaping, b escaping — verifies per-param resolution"
3131        );
3132    }
3133}