Skip to main content

shape_vm/mir/
storage_planning.rs

1//! Storage Planning Pass — decides the runtime storage class for each binding.
2//!
3//! After MIR lowering and borrow analysis, this pass examines each local slot
4//! and assigns a `BindingStorageClass`:
5//!
6//! - `Direct`: Default for bindings that are never captured, never aliased, never escape.
7//! - `UniqueHeap`: For bindings that escape into closures with mutation (need Arc wrapper).
8//! - `SharedCow`: For `var` bindings that are aliased AND mutated (copy-on-write),
9//!   or for escaped mutable aliased bindings.
10//! - `Reference`: For bindings that hold first-class references.
11//! - `Deferred`: Only if analysis was incomplete (had fallbacks).
12//!
13//! The pass also computes `EscapeStatus` for each slot:
14//! - `Local`: Stays within the declaring scope.
15//! - `Captured`: Captured by a closure.
16//! - `Escaped`: Flows to the return slot (escapes the function).
17//!
18//! Escape status drives storage decisions (escaped+aliased+mutated → SharedCow)
19//! and is consumed by the post-solve relaxation pass to determine whether
20//! local containers can safely hold references.
21//!
22//! The pass runs once per function and produces a `StoragePlan` consumed by codegen.
23
24use std::collections::{HashMap, HashSet};
25use std::sync::OnceLock;
26
27use crate::mir::analysis::{BorrowAnalysis, FunctionBorrowSummary};
28use crate::mir::types::*;
29use crate::type_tracking::{
30    Aliasability, BindingOwnershipClass, BindingSemantics, BindingStorageClass, EscapeStatus,
31    MutationCapability,
32};
33
34/// Phase V0.a bisect-safety gate; remove once ownership Phase 1 lands.
35///
36/// When `true` (the default), `var` bindings — i.e. bindings whose ownership
37/// class is `BindingOwnershipClass::Flexible` — are assigned
38/// `BindingStorageClass::SharedCow` regardless of aliasing/mutation status.
39/// This matches the ownership-aware runtime v2 contract: `var` opts into
40/// SharedCow aliasing by construction. Flip the flag off (set
41/// `SHAPE_V2_VAR_SHAREDCOW=0`) to fall back to the narrower pre-V0.a rule
42/// that only promotes to SharedCow when the binding is both aliased AND
43/// mutated. This is a temporary bisect safety net; once the rest of ownership
44/// Phase 1 lands, the flag (and this helper) can be deleted.
45///
46/// The env var is read once per process and cached in a `OnceLock`. Values
47/// `0`/`false`/`off`/`no` (case-insensitive, trimmed) or an empty string
48/// disable the flag; anything else (or the env var being unset) leaves the
49/// default `true`.
50fn var_sharedcow_default_enabled() -> bool {
51    static CACHED: OnceLock<bool> = OnceLock::new();
52    *CACHED.get_or_init(|| match std::env::var("SHAPE_V2_VAR_SHAREDCOW") {
53        // Explicit opt-out values disable the flag.
54        Ok(v) => !matches!(
55            v.trim(),
56            "0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No" | ""
57        ),
58        // Not set → default on.
59        Err(_) => true,
60    })
61}
62
63/// Maximum element count for a non-escaping aggregate to be eligible for the
64/// stack-allocated / inline optimization hint. Arrays larger than this are
65/// never flagged, even if they don't escape. The threshold matches the
66/// SmallVec inline-capacity target discussed in the Phase D design notes
67/// (8 elements × 8 bytes = 64 B, one cache line).
68pub const INLINE_ARRAY_MAX_ELEMENTS: usize = 8;
69
70/// The computed storage plan for a single function.
71#[derive(Debug, Clone)]
72pub struct StoragePlan {
73    /// Maps each local slot to its decided storage class.
74    pub slot_classes: HashMap<SlotId, BindingStorageClass>,
75    /// Maps each local slot to its enriched binding semantics.
76    pub slot_semantics: HashMap<SlotId, BindingSemantics>,
77    /// Optimization hint (Phase D.2 infrastructure): slots that hold a small,
78    /// compile-time-sized aggregate (array/tuple literal) that is provably
79    /// non-escaping, non-captured, and non-aliased. The value is the element
80    /// count.
81    ///
82    /// Consumers may choose to:
83    ///   * emit inline-on-stack storage (SROA — eliminate the allocation),
84    ///   * pick a SmallVec-backed `HeapValue::Array` path, or
85    ///   * ignore the hint entirely (semantics-preserving).
86    ///
87    /// Today no consumer acts on the hint; it is recorded so that future
88    /// codegen passes (inline arrays, scalar replacement of aggregates) can
89    /// activate without re-running escape analysis.
90    pub inline_array_sizes: HashMap<SlotId, usize>,
91    /// Closure Spec Phase B: slots that hold closure values (defined by a
92    /// `ClosureCapture` statement) and that provably do not escape their
93    /// enclosing function via any of the §2.1 escape vectors
94    /// (return, container store, field/index/deref write, capture by an
95    /// escaping closure, task boundary, call-site argument, or
96    /// `UniqueHeap`/`SharedCow` promotion).
97    ///
98    /// Consumed by Phase C's per-closure specialization decision: when a
99    /// closure slot is in this set, the specialization may stack-allocate the
100    /// closure and inline its body into the receiving specialization instead
101    /// of heap-allocating a `TypedClosure`. Slots NOT in this set are
102    /// considered escaping and must use the heap variant.
103    ///
104    /// Today no consumer acts on the set (Phase C is not landed). It is
105    /// recorded so the specialization pass can activate without re-running
106    /// escape analysis.
107    pub non_escaping_closure_slots: HashSet<SlotId>,
108}
109
110/// Input bundle for the storage planner.
111pub struct StoragePlannerInput<'a> {
112    /// The MIR function to plan storage for.
113    pub mir: &'a MirFunction,
114    /// Borrow analysis results (includes liveness).
115    pub analysis: &'a BorrowAnalysis,
116    /// Per-slot ownership/storage semantics from the compiler's type tracker.
117    pub binding_semantics: &'a HashMap<u16, BindingSemantics>,
118    /// Slots captured by any closure in this function.
119    pub closure_captures: &'a HashSet<SlotId>,
120    /// Slots that are mutated inside a closure body.
121    pub mutable_captures: &'a HashSet<SlotId>,
122    /// Whether MIR lowering had fallbacks (incomplete analysis).
123    pub had_fallbacks: bool,
124    /// Previously-computed callee borrow summaries, keyed by function name.
125    /// Used by the closure-escape analysis (Closure Spec Phase B) to check
126    /// whether a closure passed as a call-site argument may escape into the
127    /// callee. `None` (or missing entries) is treated conservatively — the
128    /// closure is assumed to escape. Populated from
129    /// `Compiler::function_borrow_summaries`.
130    pub callee_summaries: Option<&'a HashMap<String, FunctionBorrowSummary>>,
131}
132
133/// Scan MIR statements and terminators to find slots captured by closures.
134///
135/// Returns `(all_captures, mutable_captures)`:
136/// - `all_captures`: slots referenced in `ClosureCapture` statements
137/// - `mutable_captures`: subset of captured slots that are assigned more than
138///   once in the function (i.e., re-assigned after initial definition). A slot
139///   with only its initial definition assignment is not considered mutably captured.
140pub fn collect_closure_captures(mir: &MirFunction) -> (HashSet<SlotId>, HashSet<SlotId>) {
141    let mut all_captures = HashSet::new();
142    let mut assign_counts: HashMap<SlotId, u32> = HashMap::new();
143
144    for block in mir.iter_blocks() {
145        for stmt in &block.statements {
146            match &stmt.kind {
147                StatementKind::ClosureCapture { operands, .. } => {
148                    for op in operands {
149                        if let Some(slot) = operand_root_slot(op) {
150                            all_captures.insert(slot);
151                        }
152                    }
153                }
154                StatementKind::Assign(place, _) => {
155                    if let Place::Local(slot) = place {
156                        *assign_counts.entry(*slot).or_insert(0) += 1;
157                    }
158                }
159                _ => {}
160            }
161        }
162    }
163
164    // A slot is "mutably captured" if it is captured AND assigned more than once
165    // (meaning it has re-assignments beyond its initial definition).
166    let mutable_captures: HashSet<SlotId> = all_captures
167        .iter()
168        .filter(|slot| assign_counts.get(slot).copied().unwrap_or(0) > 1)
169        .copied()
170        .collect();
171
172    (all_captures, mutable_captures)
173}
174
175/// Extract the root SlotId from an operand, if it references a local.
176fn operand_root_slot(op: &Operand) -> Option<SlotId> {
177    match op {
178        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
179            Some(place.root_local())
180        }
181        Operand::Constant(_) => None,
182    }
183}
184
185/// Check whether a slot has any active loan (borrow) in the analysis.
186/// A slot with loans is holding or being borrowed as a reference.
187fn slot_has_active_loans(slot: SlotId, analysis: &BorrowAnalysis) -> bool {
188    for loan_info in analysis.loans.values() {
189        if loan_info.borrowed_place.root_local() == slot {
190            return true;
191        }
192    }
193    false
194}
195
196/// Check whether a slot is aliased — it appears as an operand in more than
197/// one `Assign` rvalue across the function, or it is captured.
198fn slot_is_aliased(slot: SlotId, mir: &MirFunction, closure_captures: &HashSet<SlotId>) -> bool {
199    if closure_captures.contains(&slot) {
200        return true;
201    }
202
203    let mut use_count = 0u32;
204    for block in mir.iter_blocks() {
205        for stmt in &block.statements {
206            if let StatementKind::Assign(_, rvalue) = &stmt.kind {
207                if rvalue_uses_slot(rvalue, slot) {
208                    use_count += 1;
209                    if use_count > 1 {
210                        return true;
211                    }
212                }
213            }
214        }
215        // Also check terminators for uses
216        if let TerminatorKind::Call { func, args, .. } = &block.terminator.kind {
217            if operand_uses_slot(func, slot) {
218                use_count += 1;
219            }
220            for arg in args {
221                if operand_uses_slot(arg, slot) {
222                    use_count += 1;
223                }
224            }
225            if use_count > 1 {
226                return true;
227            }
228        }
229    }
230    false
231}
232
233/// Check if a slot is mutated in the function (assigned to after initial definition).
234fn slot_is_mutated(slot: SlotId, mir: &MirFunction) -> bool {
235    let mut assign_count = 0u32;
236    for block in mir.iter_blocks() {
237        for stmt in &block.statements {
238            if let StatementKind::Assign(Place::Local(s), _) = &stmt.kind {
239                if *s == slot {
240                    assign_count += 1;
241                    if assign_count > 1 {
242                        return true;
243                    }
244                }
245            }
246        }
247    }
248    false
249}
250
251/// Check whether an rvalue uses (reads from) a given slot.
252fn rvalue_uses_slot(rvalue: &Rvalue, slot: SlotId) -> bool {
253    match rvalue {
254        Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => {
255            operand_uses_slot(op, slot)
256        }
257        Rvalue::Borrow(_, place) => place.root_local() == slot,
258        Rvalue::BinaryOp(_, lhs, rhs) => {
259            operand_uses_slot(lhs, slot) || operand_uses_slot(rhs, slot)
260        }
261        Rvalue::Aggregate(ops) => ops.iter().any(|op| operand_uses_slot(op, slot)),
262        Rvalue::EnumTest { operand, .. }
263        | Rvalue::EnumPayload { operand, .. }
264        | Rvalue::TypePatternTest { operand, .. }
265        | Rvalue::EnumDiscriminantTest { operand, .. } => operand_uses_slot(operand, slot),
266    }
267}
268
269/// Check whether an operand references a given slot.
270fn operand_uses_slot(op: &Operand, slot: SlotId) -> bool {
271    match op {
272        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
273            place.root_local() == slot
274        }
275        Operand::Constant(_) => false,
276    }
277}
278
279/// Run the storage planning pass on a single function.
280///
281/// The algorithm examines each local slot and decides its storage class:
282///
283/// 1. If `had_fallbacks` is true, all slots remain `Deferred` (analysis incomplete).
284/// 2. For each slot, check closure captures, mutations, aliasing, and loans.
285/// 3. Assign the appropriate `BindingStorageClass`.
286pub fn plan_storage(input: &StoragePlannerInput<'_>) -> StoragePlan {
287    let mut slot_classes = HashMap::new();
288    let mut slot_semantics = HashMap::new();
289
290    // If MIR lowering had fallbacks, we cannot trust the analysis.
291    // Leave everything Deferred so codegen uses conservative paths.
292    if input.had_fallbacks {
293        for slot_idx in 0..input.mir.num_locals {
294            let slot = SlotId(slot_idx);
295            slot_classes.insert(slot, BindingStorageClass::Deferred);
296            slot_semantics.insert(
297                slot,
298                BindingSemantics {
299                    ownership_class: BindingOwnershipClass::OwnedImmutable,
300                    storage_class: BindingStorageClass::Deferred,
301                    aliasability: Aliasability::Unique,
302                    mutation_capability: MutationCapability::Immutable,
303                    escape_status: EscapeStatus::Local,
304                    return_ownership_hint: None,
305                },
306            );
307        }
308        return StoragePlan {
309            slot_classes,
310            slot_semantics,
311            inline_array_sizes: HashMap::new(),
312            non_escaping_closure_slots: HashSet::new(),
313        };
314    }
315
316    let var_sharedcow_enabled = var_sharedcow_default_enabled();
317    for slot_idx in 0..input.mir.num_locals {
318        let slot = SlotId(slot_idx);
319        let (storage_class, semantics) =
320            decide_slot_storage(slot, input, var_sharedcow_enabled);
321        slot_classes.insert(slot, storage_class);
322        slot_semantics.insert(slot, semantics);
323    }
324
325    // Detect small non-escaping aggregates and record the hint. This is
326    // gathered after slot decisions so we can consult `slot_semantics` for the
327    // authoritative escape/aliasing verdict.
328    let inline_array_sizes = detect_inline_array_candidates(input, &slot_semantics);
329
330    // Closure Spec Phase B: classify every closure slot (each slot defined by
331    // a `ClosureCapture` statement) as escaping or non-escaping, using the
332    // full §2.1 escape-vector table plus a fixed-point over transitive
333    // closure captures.
334    let non_escaping_closure_slots =
335        detect_non_escaping_closure_slots(input, &slot_classes);
336
337    // Closure Spec Phase D: promote the storage class of outer slots whose
338    // ONLY heap-indirection driver is a mutable capture by a non-escaping
339    // closure to `LocalMutablePtr`. The previous pass assigned `UniqueHeap`
340    // to such slots (Rule 2 in `decide_slot_storage`) — that was the v1-style
341    // "box into SharedCell on capture" decision. In v2 the closure env holds
342    // a typed stack pointer instead; the binding can stay on the frame.
343    //
344    // Preconditions (all must hold):
345    //   1. The slot is the root of a mutably-captured operand in a
346    //      `ClosureCapture` statement.
347    //   2. The closure slot that captures it is in
348    //      `non_escaping_closure_slots`.
349    //   3. The slot is not itself a first-class reference or promoted to a
350    //      heap-aliased class for some other reason (e.g. also stored into
351    //      a collection).
352    //
353    // Escape fallback (§4.3): for `let mut` + escaping closure with a mutable
354    // capture, `compile_expr_closure` emits `B0003`. For `var` + escaping
355    // closure, the SharedCow path is unchanged.
356    promote_local_mutable_ptr_slots(
357        input,
358        &non_escaping_closure_slots,
359        &mut slot_classes,
360        &mut slot_semantics,
361    );
362
363    StoragePlan {
364        slot_classes,
365        slot_semantics,
366        inline_array_sizes,
367        non_escaping_closure_slots,
368    }
369}
370
371/// Phase D — promote outer-slot storage class to `LocalMutablePtr` when the
372/// slot is captured by a non-escaping closure.
373///
374/// This runs AFTER the main storage-class decision pass and AFTER the
375/// `non_escaping_closure_slots` computation. It walks every `ClosureCapture`
376/// in the MIR; for each operand whose root slot currently has a conservative
377/// class (`Direct` / `Deferred` / `UniqueHeap`) it promotes to
378/// `LocalMutablePtr`. The MIR statement does not distinguish
379/// mutable-vs-immutable operands — the closure-compiler side inspects the
380/// closure body via `EnvironmentAnalyzer::analyze_function_with_mutability`
381/// and only emits `LoadCaptureMutPtr<T>` / `StoreCaptureMutPtr<T>` for
382/// captures that the body actually mutates. Unused promotions are harmless:
383/// `LocalMutablePtr` carries the same "slot stays on the frame" meaning as
384/// `Direct` and reads through the existing non-closure code paths unchanged.
385///
386/// Conservative: if the outer slot has any other escape vector (it is also
387/// returned, stored in a container, or captured by a different escaping
388/// closure), `decide_slot_storage` already put it in a non-promotable class
389/// (e.g. `SharedCow`) and we leave it alone.
390fn promote_local_mutable_ptr_slots(
391    input: &StoragePlannerInput<'_>,
392    non_escaping_closure_slots: &HashSet<SlotId>,
393    slot_classes: &mut HashMap<SlotId, BindingStorageClass>,
394    slot_semantics: &mut HashMap<SlotId, BindingSemantics>,
395) {
396    for block in input.mir.iter_blocks() {
397        for stmt in &block.statements {
398            let StatementKind::ClosureCapture {
399                closure_slot,
400                operands,
401                ..
402            } = &stmt.kind
403            else {
404                continue;
405            };
406
407            if !non_escaping_closure_slots.contains(closure_slot) {
408                continue;
409            }
410
411            for op in operands {
412                let Some(root) = operand_root_slot(op) else {
413                    continue;
414                };
415                let Some(current) = slot_classes.get(&root).copied() else {
416                    continue;
417                };
418                match current {
419                    BindingStorageClass::UniqueHeap
420                    | BindingStorageClass::Direct
421                    | BindingStorageClass::Deferred => {
422                        slot_classes.insert(root, BindingStorageClass::LocalMutablePtr);
423                        if let Some(sem) = slot_semantics.get_mut(&root) {
424                            sem.storage_class = BindingStorageClass::LocalMutablePtr;
425                        }
426                    }
427                    // SharedCow / Reference / LocalMutablePtr: leave alone.
428                    _ => {}
429                }
430            }
431        }
432    }
433}
434
435/// Scan MIR for aggregate assignments that are safe to inline on the stack.
436///
437/// A slot is a candidate when **all** of the following hold:
438/// - It is assigned exactly one `Rvalue::Aggregate` with `N <= INLINE_ARRAY_MAX_ELEMENTS`.
439/// - Its computed `escape_status` is `Local` (does not flow to the return slot).
440/// - It is not captured by any closure.
441/// - It is not re-assigned after the aggregate initialization (otherwise the
442///   slot can't be treated as a fixed-shape value).
443/// - It is not mutated through index assignment (future work: inline arrays
444///   with index writes would need per-element tracking).
445///
446/// The hint is purely advisory — emitting it does not commit to any specific
447/// codegen strategy. Zero-size aggregates are also skipped since they add no
448/// allocation pressure to begin with.
449fn detect_inline_array_candidates(
450    input: &StoragePlannerInput<'_>,
451    slot_semantics: &HashMap<SlotId, BindingSemantics>,
452) -> HashMap<SlotId, usize> {
453    let mut aggregate_sizes: HashMap<SlotId, usize> = HashMap::new();
454    let mut disqualified: HashSet<SlotId> = HashSet::new();
455
456    for block in input.mir.iter_blocks() {
457        for stmt in &block.statements {
458            let StatementKind::Assign(place, rvalue) = &stmt.kind else {
459                continue;
460            };
461            let Place::Local(slot) = place else {
462                // A projection write (e.g. `arr[i] = x`) into a candidate
463                // slot disqualifies it — the aggregate shape is mutable.
464                disqualified.insert(place.root_local());
465                continue;
466            };
467
468            match rvalue {
469                Rvalue::Aggregate(ops) => {
470                    // Only the first aggregate assignment counts. A later
471                    // reassignment (even another aggregate) means the slot
472                    // isn't a fixed-shape value.
473                    if aggregate_sizes.contains_key(slot) {
474                        disqualified.insert(*slot);
475                    } else {
476                        aggregate_sizes.insert(*slot, ops.len());
477                    }
478                }
479                _ => {
480                    // Any non-aggregate assignment (re-binding, function
481                    // result, etc.) after the aggregate disqualifies the slot.
482                    if aggregate_sizes.contains_key(slot) {
483                        disqualified.insert(*slot);
484                    }
485                }
486            }
487        }
488    }
489
490    let mut hints = HashMap::new();
491    for (slot, size) in aggregate_sizes {
492        if disqualified.contains(&slot) {
493            continue;
494        }
495        if size == 0 || size > INLINE_ARRAY_MAX_ELEMENTS {
496            continue;
497        }
498        let Some(sem) = slot_semantics.get(&slot) else {
499            continue;
500        };
501        if sem.escape_status != EscapeStatus::Local {
502            continue;
503        }
504        // Be defensive: any aliasing (shared-immutable or shared-mutable)
505        // rules out the SROA-style optimization, even though the aggregate
506        // itself would remain addressable on the stack.
507        if sem.aliasability != Aliasability::Unique {
508            continue;
509        }
510        if input.closure_captures.contains(&slot) {
511            continue;
512        }
513        hints.insert(slot, size);
514    }
515    hints
516}
517
518// ── Closure Spec Phase B: non-escape detection for closure slots ────────────
519//
520// The analysis implements the 10-row escape-vector table from
521// `docs/v2-closure-specialization.md` §2.1. Its output is the set of closure
522// slots that are provably non-escaping, recorded on `StoragePlan` for the
523// Phase C specialization pass to consume.
524//
525// Soundness discipline: when an escape vector cannot be decided (missing
526// callee summary, unknown operand shape), the slot is marked as escaping.
527// Any precision lost here is recoverable by Phase C's monomorphization work;
528// the cost of a false "non-escaping" verdict is a stack-pointer outliving a
529// frame, which is unacceptable.
530
531/// Collect every slot that is defined as a closure value (the destination of
532/// a `ClosureCapture` statement). The `closure_slot` field of a
533/// `ClosureCapture` identifies the slot that will hold the closure object
534/// once the follow-up `Assign(..., ClosurePlaceholder)` completes the
535/// definition. Functions with no closure literals produce an empty set.
536fn collect_closure_slots(mir: &MirFunction) -> HashSet<SlotId> {
537    let mut slots = HashSet::new();
538    for block in mir.iter_blocks() {
539        for stmt in &block.statements {
540            if let StatementKind::ClosureCapture { closure_slot, .. } = &stmt.kind {
541                slots.insert(*closure_slot);
542            }
543        }
544    }
545    slots
546}
547
548/// Build the transitive closure-capture graph: if closure A captures closure
549/// B, the map records `A -> {B}`. Used for the §2.4 fixed-point — if A
550/// escapes, B escapes too because it is carried inside A's capture layout.
551fn build_closure_capture_graph(
552    mir: &MirFunction,
553    closure_slots: &HashSet<SlotId>,
554) -> HashMap<SlotId, HashSet<SlotId>> {
555    let mut graph: HashMap<SlotId, HashSet<SlotId>> = HashMap::new();
556    for block in mir.iter_blocks() {
557        for stmt in &block.statements {
558            if let StatementKind::ClosureCapture {
559                closure_slot,
560                operands,
561                ..
562            } = &stmt.kind
563            {
564                for op in operands {
565                    let Some(root) = operand_root_slot(op) else {
566                        continue;
567                    };
568                    if closure_slots.contains(&root) {
569                        graph
570                            .entry(*closure_slot)
571                            .or_default()
572                            .insert(root);
573                    }
574                }
575            }
576        }
577    }
578    graph
579}
580
581/// §2.1 direct-vector escape check for a single closure slot `c`.
582///
583/// Walks the MIR once per call. Returns `true` if any of rows 1-3, 5-7, 9-10
584/// fire; row 4 (transitive capture) and row 8 (call-argument through callee
585/// summary) are handled by `detect_non_escaping_closure_slots` after direct
586/// vectors are classified.
587///
588/// The walk uses a single-pass monotonic scan: starting from the singleton
589/// tracked set `{c}`, each `Assign(Place::Local(dest), Rvalue::Use(Copy/Move
590/// of a tracked slot))` widens the tracked set to include `dest`. This mirrors
591/// the existing `slot_flows_to_return` pattern but generalized to every sink
592/// the table names, without requiring a separate dataflow pass.
593fn closure_slot_escapes_direct(
594    c: SlotId,
595    input: &StoragePlannerInput<'_>,
596) -> bool {
597    let mir = input.mir;
598
599    // Row 10: the semantics planner promoted the slot to heap-aliased
600    // storage. Any such promotion escapes by construction.
601    if let Some(sem) = input.binding_semantics.get(&c.0) {
602        match sem.storage_class {
603            BindingStorageClass::UniqueHeap | BindingStorageClass::SharedCow => {
604                return true;
605            }
606            _ => {}
607        }
608    }
609
610    // Iteratively widen the tracked set as the closure flows through local
611    // aliases. Each pass also evaluates every escape vector against the
612    // current tracked set — reaching any escape sink is a definite "yes".
613    let mut tracked: HashSet<SlotId> = HashSet::new();
614    tracked.insert(c);
615
616    let mut changed = true;
617    while changed {
618        changed = false;
619
620        for block in mir.iter_blocks() {
621            for stmt in &block.statements {
622                match &stmt.kind {
623                    // Row 1 (return), Row 2 (Aggregate), Row 3 (struct
624                    // field), Row 9 (deref), and local-alias propagation.
625                    StatementKind::Assign(place, rvalue) => {
626                        let reads_tracked = rvalue_uses_any_slot(rvalue, &tracked);
627                        match place {
628                            Place::Local(dest) => {
629                                // Row 1: dest is the return slot.
630                                if *dest == SlotId(0) && reads_tracked {
631                                    return true;
632                                }
633                                // Local-alias propagation: widen the tracked
634                                // set so downstream vectors see the alias.
635                                if reads_tracked && tracked.insert(*dest) {
636                                    changed = true;
637                                }
638                            }
639                            // Row 3: struct-field write `foo.f = c`.
640                            Place::Field(..) => {
641                                if reads_tracked {
642                                    return true;
643                                }
644                            }
645                            // Row 2 subcase / Row 9 variant: index/deref
646                            // writes both materialize the value into some
647                            // other container.
648                            Place::Index(..) | Place::Deref(..) => {
649                                if reads_tracked {
650                                    return true;
651                                }
652                            }
653                        }
654
655                        // Row 2: `Rvalue::Aggregate` feeding any slot — the
656                        // aggregate itself is the escaping container.
657                        if let Rvalue::Aggregate(ops) = rvalue {
658                            if ops.iter().any(|op| operand_uses_any_slot(op, &tracked)) {
659                                return true;
660                            }
661                        }
662                    }
663                    // Row 2: store into array / object / enum literal.
664                    StatementKind::ArrayStore { operands, .. }
665                    | StatementKind::ObjectStore { operands, .. }
666                    | StatementKind::EnumStore { operands, .. } => {
667                        if operands.iter().any(|op| operand_uses_any_slot(op, &tracked)) {
668                            return true;
669                        }
670                    }
671                    // Rows 5 + 6: detached or structured task boundary.
672                    // §5.5 allows stack closures across structured boundaries
673                    // in the future, but Phase B takes the conservative
674                    // verdict — any boundary is escape.
675                    StatementKind::TaskBoundary(operands, _) => {
676                        if operands.iter().any(|op| operand_uses_any_slot(op, &tracked)) {
677                            return true;
678                        }
679                    }
680                    // Row 4 direct contribution: if we find a closure
681                    // capturing a tracked slot and that enclosing closure
682                    // later turns out to escape, the tracked slot escapes
683                    // transitively. Handled by the fixed-point in
684                    // `detect_non_escaping_closure_slots`.
685                    StatementKind::ClosureCapture { .. }
686                    | StatementKind::Drop(_)
687                    | StatementKind::Nop => {}
688                }
689            }
690
691            // Row 8: call-site argument. Conservative default — any call
692            // with a tracked slot in its args escapes, unless the callee is a
693            // statically-known function whose `FunctionBorrowSummary` marks
694            // the corresponding param as non-escaping.
695            if let TerminatorKind::Call { func, args, .. } = &block.terminator.kind {
696                // Row 7: `snapshot()` is an opaque FFI in v1 — there is no
697                // dedicated MIR terminator, so any named call to `snapshot`
698                // must be treated as escape. The full treatment lives in §9
699                // open question #7.
700                let callee_name = match func {
701                    Operand::Constant(MirConstant::Function(name)) => Some(name.as_str()),
702                    _ => None,
703                };
704                if callee_name == Some("snapshot") {
705                    if args.iter().any(|op| operand_uses_any_slot(op, &tracked)) {
706                        return true;
707                    }
708                }
709
710                let callee_summary = callee_name
711                    .and_then(|n| input.callee_summaries.and_then(|m| m.get(n)));
712
713                for (arg_idx, arg) in args.iter().enumerate() {
714                    if !operand_uses_any_slot(arg, &tracked) {
715                        continue;
716                    }
717                    // With a summary + a matching param slot, we can trust
718                    // the callee's per-param escape bit. Otherwise: escape.
719                    match callee_summary {
720                        Some(summary) if arg_idx < summary.closure_param_escapes.len() => {
721                            if summary.closure_param_escapes[arg_idx] {
722                                return true;
723                            }
724                            // else: callee is transparent for this arg;
725                            // continue checking other vectors.
726                        }
727                        _ => return true,
728                    }
729                }
730
731                // Indirect call target itself: if the call *target* is the
732                // tracked slot, we treat it as non-escape (the closure is
733                // invoked in-place). Not a separate vector.
734                let _ = func; // explicitly documenting intent
735            }
736        }
737    }
738
739    false
740}
741
742/// §2.4 fixed-point over the closure capture graph: if closure A escapes,
743/// every closure it captures (B in `graph[A]`) also escapes. The worklist
744/// iterates until the escaping set stops growing — monotone, terminates in
745/// at most `|closure_slots|` passes.
746fn propagate_transitive_closure_escape(
747    closure_slots: &HashSet<SlotId>,
748    capture_graph: &HashMap<SlotId, HashSet<SlotId>>,
749    escaping: &mut HashSet<SlotId>,
750) {
751    let mut changed = true;
752    while changed {
753        changed = false;
754        for &a in closure_slots {
755            if !escaping.contains(&a) {
756                continue;
757            }
758            let Some(captured_closures) = capture_graph.get(&a) else {
759                continue;
760            };
761            for &b in captured_closures {
762                if escaping.insert(b) {
763                    changed = true;
764                }
765            }
766        }
767    }
768}
769
770/// Does the MIR contain any named call to `snapshot`, regardless of args?
771///
772/// Closure spec Phase G §5.6: `snapshot()` captures the full VM state into
773/// `SerializableCallFrame` via the interpreter's locals array. Stack-allocated
774/// closures live in Cranelift StackSlots and are invisible to that
775/// serializer, so any function that may invoke `snapshot()` must not
776/// stack-allocate its closures — independent of whether a specific closure
777/// flows into `snapshot()`'s argument list. The spec's "forces deopt first"
778/// policy reduces to "promote every closure in the function to heap." Phase G
779/// implements the conservative half (§5.6 short-circuit); the precise
780/// per-closure deopt-with-rematerialization path is future work once MIR
781/// gains a first-class `TerminatorKind::Snapshot` (§9 open question #7).
782fn mir_contains_snapshot_call(mir: &MirFunction) -> bool {
783    for block in mir.iter_blocks() {
784        if let TerminatorKind::Call { func, .. } = &block.terminator.kind {
785            if let Operand::Constant(MirConstant::Function(name)) = func {
786                if name == "snapshot" {
787                    return true;
788                }
789            }
790        }
791    }
792    false
793}
794
795/// Populate `StoragePlan.non_escaping_closure_slots` for the function under
796/// analysis. Orchestrates: (1) collect closure slots, (2) classify each via
797/// §2.1 direct vectors, (3) run the §2.4 transitive-capture fixed-point, (4)
798/// invert — any closure slot NOT in the escaping set is non-escaping.
799fn detect_non_escaping_closure_slots(
800    input: &StoragePlannerInput<'_>,
801    _slot_classes: &HashMap<SlotId, BindingStorageClass>,
802) -> HashSet<SlotId> {
803    let closure_slots = collect_closure_slots(input.mir);
804    if closure_slots.is_empty() {
805        return HashSet::new();
806    }
807
808    // Closure spec Phase G §5.6: function-wide `snapshot()` escape.
809    // If any terminator in the function calls `snapshot`, force every
810    // closure slot to heap — stack closures cannot survive the VM-state
811    // serialization path that snapshot takes. This is the conservative
812    // complement to Phase G's JIT-side "snapshot forces deopt first"
813    // policy: the MIR planner takes the easy win (heap-allocate every
814    // closure in a snapshottable function) so we do not need to emit a
815    // JIT deopt guard for every snapshot-reachable stack slot.
816    if mir_contains_snapshot_call(input.mir) {
817        // All closure slots escape; return the empty "non-escaping" set.
818        return HashSet::new();
819    }
820
821    // Step 1: direct-vector escape verdict per closure slot.
822    let mut escaping: HashSet<SlotId> = HashSet::new();
823    for &c in &closure_slots {
824        if closure_slot_escapes_direct(c, input) {
825            escaping.insert(c);
826        }
827    }
828
829    // Step 2: transitive propagation via the capture graph.
830    let capture_graph = build_closure_capture_graph(input.mir, &closure_slots);
831    propagate_transitive_closure_escape(&closure_slots, &capture_graph, &mut escaping);
832
833    // Step 3: invert — slots not in `escaping` are non-escaping.
834    closure_slots
835        .difference(&escaping)
836        .copied()
837        .collect()
838}
839
840/// Does `rvalue` read from any slot in `slots`? Covers all rvalue shapes.
841fn rvalue_uses_any_slot(rvalue: &Rvalue, slots: &HashSet<SlotId>) -> bool {
842    match rvalue {
843        Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => {
844            operand_uses_any_slot(op, slots)
845        }
846        Rvalue::Borrow(_, place) => slots.contains(&place.root_local()),
847        Rvalue::BinaryOp(_, lhs, rhs) => {
848            operand_uses_any_slot(lhs, slots) || operand_uses_any_slot(rhs, slots)
849        }
850        Rvalue::Aggregate(ops) => ops.iter().any(|op| operand_uses_any_slot(op, slots)),
851        Rvalue::EnumTest { operand, .. }
852        | Rvalue::EnumPayload { operand, .. }
853        | Rvalue::TypePatternTest { operand, .. }
854        | Rvalue::EnumDiscriminantTest { operand, .. } => operand_uses_any_slot(operand, slots),
855    }
856}
857
858/// Does `op` reference a slot in `slots`?
859fn operand_uses_any_slot(op: &Operand, slots: &HashSet<SlotId>) -> bool {
860    match op {
861        Operand::Copy(place) | Operand::Move(place) | Operand::MoveExplicit(place) => {
862            slots.contains(&place.root_local())
863        }
864        Operand::Constant(_) => false,
865    }
866}
867
868/// Decide the storage class for a single slot, returning both the storage class
869/// and enriched binding semantics.
870/// Decide the storage class and enriched semantics for a single slot.
871///
872/// ## Decision matrix
873///
874/// Priority order (first matching rule wins):
875///
876/// | #  | Condition                                      | Storage class  |
877/// |----|------------------------------------------------|----------------|
878/// | 0  | Explicit `Reference` already set               | `Reference`    |
879/// | 1  | Slot holds a first-class reference              | `Reference`    |
880/// | 1b | **V0.a**: `var` (Flexible) + flag on           | `SharedCow`    |
881/// | 2  | Captured by closure with mutation               | `UniqueHeap`   |
882/// | 3  | `var` (Flexible) + aliased + mutated            | `SharedCow`    |
883/// | 3b | Escaped + aliased + mutated (any ownership)     | `SharedCow`    |
884/// | 4  | Everything else                                 | `Direct`       |
885///
886/// Rule 1b (Phase V0.a): when `SHAPE_V2_VAR_SHAREDCOW` is enabled (the
887/// default), every `var` binding is promoted to `SharedCow` regardless of
888/// aliasing/mutation. The legacy Rule 3 remains as a safety net and as the
889/// behavior observed when the flag is disabled. Rule 1b runs BEFORE Rule 2
890/// so that a `var` slot that happens to be mutably captured stays `SharedCow`
891/// rather than being routed through the `UniqueHeap` → `LocalMutablePtr`
892/// promotion: the spec's Phase 4 contract is that `var` is aliased by
893/// construction.
894///
895/// Notes:
896/// - "Aliased" means either captured by a closure or referenced from multiple
897///   MIR places (e.g. through a borrow chain).
898/// - `UniqueHeap` and `SharedCow` both result in heap boxing at runtime, but
899///   `SharedCow` adds copy-on-write semantics for safe shared mutation.
900/// - Immutable closure captures stay `Direct` — the closure gets a plain copy.
901fn decide_slot_storage(
902    slot: SlotId,
903    input: &StoragePlannerInput<'_>,
904    var_sharedcow_enabled: bool,
905) -> (BindingStorageClass, BindingSemantics) {
906    let is_captured = input.closure_captures.contains(&slot);
907    let is_mutably_captured = input.mutable_captures.contains(&slot);
908    let _has_loans = slot_has_active_loans(slot, input.analysis);
909    let is_mutated = slot_is_mutated(slot, input.mir);
910    let is_aliased = slot_is_aliased(slot, input.mir, input.closure_captures);
911
912    // Look up ownership class from binding semantics
913    let ownership = input
914        .binding_semantics
915        .get(&slot.0)
916        .map(|s| s.ownership_class);
917
918    // Check if the binding already has an explicit storage class set
919    let explicit_storage = input
920        .binding_semantics
921        .get(&slot.0)
922        .map(|s| s.storage_class);
923
924    let is_escaped = detect_escape_status(slot, input.mir, input.closure_captures)
925        == EscapeStatus::Escaped;
926
927    let storage_class = if let Some(BindingStorageClass::Reference) = explicit_storage {
928        // Already marked as a reference binding — preserve it.
929        BindingStorageClass::Reference
930    } else if slot_holds_reference(slot, input.mir) {
931        // Rule 1: Bindings that hold first-class references.
932        BindingStorageClass::Reference
933    } else if matches!(ownership, Some(BindingOwnershipClass::Flexible)) && var_sharedcow_enabled {
934        // Rule 1b (Phase V0.a): `var` bindings are SharedCow by default when
935        // the `SHAPE_V2_VAR_SHAREDCOW` flag is on. `var`'s ownership class is
936        // `Flexible`, which the spec's Phase 4 defines as "aliased by
937        // construction" — SharedCow regardless of whether the MIR pass
938        // observed an actual alias+mutation pair. The flag defaults to on;
939        // setting the env var to `0` falls back to the narrower Rule 3.
940        BindingStorageClass::SharedCow
941    } else if is_mutably_captured {
942        // Rule 2: Captured by closure with mutation → UniqueHeap.
943        BindingStorageClass::UniqueHeap
944    } else if matches!(ownership, Some(BindingOwnershipClass::Flexible))
945        && is_aliased
946        && is_mutated
947    {
948        // Rule 3: `var` bindings that are aliased AND mutated → SharedCow.
949        // Still present as a safety net: when the V0.a flag is disabled the
950        // planner falls back to this narrower rule.
951        BindingStorageClass::SharedCow
952    } else if is_escaped && is_aliased && is_mutated {
953        // Rule 3b: Escaped mutable aliased bindings → SharedCow.
954        // Even non-Flexible bindings need COW when they escape with aliasing.
955        BindingStorageClass::SharedCow
956    } else {
957        // Rule 4: Captured by closure (immutably) — still Direct.
958        // Default: Direct storage (stack slot).
959        BindingStorageClass::Direct
960    };
961
962    // Compute enriched metadata
963    let aliasability = if is_captured || is_aliased {
964        if is_mutated {
965            Aliasability::SharedMutable
966        } else {
967            Aliasability::SharedImmutable
968        }
969    } else {
970        Aliasability::Unique
971    };
972
973    let mutation_capability = match (ownership, is_mutated) {
974        (Some(BindingOwnershipClass::OwnedImmutable), _) => MutationCapability::Immutable,
975        (Some(BindingOwnershipClass::OwnedMutable), _) => MutationCapability::LocalMutable,
976        (Some(BindingOwnershipClass::Flexible), true) => MutationCapability::SharedMutable,
977        (Some(BindingOwnershipClass::Flexible), false) => MutationCapability::Immutable,
978        (None, true) => MutationCapability::LocalMutable,
979        (None, false) => MutationCapability::Immutable,
980    };
981
982    let escape_status = detect_escape_status(slot, input.mir, input.closure_captures);
983
984    // Preserve the Phase 5.B return-ownership hint through storage planning:
985    // it was populated on the incoming binding semantics by the compiler's
986    // let-statement path and is consumed at codegen time.
987    let return_ownership_hint = input
988        .binding_semantics
989        .get(&slot.0)
990        .and_then(|s| s.return_ownership_hint);
991
992    let enriched = BindingSemantics {
993        ownership_class: ownership.unwrap_or(BindingOwnershipClass::OwnedImmutable),
994        storage_class: storage_class,
995        aliasability,
996        mutation_capability,
997        escape_status,
998        return_ownership_hint,
999    };
1000
1001    (storage_class, enriched)
1002}
1003
1004/// Detect the escape status of a slot by examining MIR dataflow.
1005///
1006/// - `Escaped`: The slot's value flows, directly or through local aliases, into
1007///   the return slot (`SlotId(0)`).
1008/// - `Captured`: The slot is captured by a closure.
1009/// - `Local`: The slot stays within the declaring scope.
1010pub fn detect_escape_status(
1011    slot: SlotId,
1012    mir: &MirFunction,
1013    closure_captures: &HashSet<SlotId>,
1014) -> EscapeStatus {
1015    if slot != SlotId(0) {
1016        let mut visited = HashSet::new();
1017        if slot_flows_to_return(slot, mir, &mut visited) {
1018            return EscapeStatus::Escaped;
1019        }
1020    }
1021
1022    if closure_captures.contains(&slot) {
1023        EscapeStatus::Captured
1024    } else {
1025        EscapeStatus::Local
1026    }
1027}
1028
1029fn slot_flows_to_return(
1030    slot: SlotId,
1031    mir: &MirFunction,
1032    visited: &mut HashSet<SlotId>,
1033) -> bool {
1034    if !visited.insert(slot) {
1035        return false;
1036    }
1037
1038    let return_slot = SlotId(0);
1039    for block in mir.iter_blocks() {
1040        for stmt in &block.statements {
1041            let StatementKind::Assign(Place::Local(dest), rvalue) = &stmt.kind else {
1042                continue;
1043            };
1044            if !rvalue_uses_slot(rvalue, slot) {
1045                continue;
1046            }
1047            if *dest == return_slot {
1048                return true;
1049            }
1050            if *dest != slot && slot_flows_to_return(*dest, mir, visited) {
1051                return true;
1052            }
1053        }
1054    }
1055
1056    false
1057}
1058
1059/// Check if a slot was assigned a `Borrow` rvalue anywhere in the function.
1060fn slot_holds_reference(slot: SlotId, mir: &MirFunction) -> bool {
1061    for block in mir.iter_blocks() {
1062        for stmt in &block.statements {
1063            if let StatementKind::Assign(Place::Local(s), Rvalue::Borrow(_, _)) = &stmt.kind {
1064                if *s == slot {
1065                    return true;
1066                }
1067            }
1068        }
1069    }
1070    false
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075    use super::*;
1076    use crate::mir::analysis::BorrowAnalysis;
1077    use crate::mir::liveness::LivenessResult;
1078    use crate::mir::types::*;
1079    use crate::type_tracking::{
1080        Aliasability, BindingOwnershipClass, BindingSemantics, BindingStorageClass, EscapeStatus,
1081        MutationCapability,
1082    };
1083
1084    fn span() -> shape_ast::ast::Span {
1085        shape_ast::ast::Span { start: 0, end: 1 }
1086    }
1087
1088    fn make_stmt(kind: StatementKind, point: u32) -> MirStatement {
1089        MirStatement {
1090            kind,
1091            span: span(),
1092            point: Point(point),
1093        }
1094    }
1095
1096    fn make_terminator(kind: TerminatorKind) -> Terminator {
1097        Terminator { kind, span: span() }
1098    }
1099
1100    fn empty_analysis() -> BorrowAnalysis {
1101        BorrowAnalysis::empty()
1102    }
1103
1104    /// Helper: create a simple MIR function with the given blocks.
1105    fn make_mir(name: &str, blocks: Vec<BasicBlock>, num_locals: u16) -> MirFunction {
1106        MirFunction {
1107            name: name.to_string(),
1108            blocks,
1109            num_locals,
1110            param_slots: vec![],
1111            param_reference_kinds: vec![],
1112            local_types: (0..num_locals).map(|_| LocalTypeInfo::Unknown).collect(),
1113            span: span(),
1114            field_name_table: std::collections::HashMap::new(),
1115            local_struct_type_names: std::collections::HashMap::new(),
1116            local_typed_array_element_types: std::collections::HashMap::new(),
1117            local_declared_scalar_types: std::collections::HashMap::new(),
1118        }
1119    }
1120
1121    // ── Test: Direct storage for simple binding ──────────────────────────
1122
1123    #[test]
1124    fn test_simple_binding_gets_direct() {
1125        // bb0: _0 = 42; return
1126        let mir = make_mir(
1127            "test_direct",
1128            vec![BasicBlock {
1129                id: BasicBlockId(0),
1130                statements: vec![make_stmt(
1131                    StatementKind::Assign(
1132                        Place::Local(SlotId(0)),
1133                        Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1134                    ),
1135                    0,
1136                )],
1137                terminator: make_terminator(TerminatorKind::Return),
1138            }],
1139            1,
1140        );
1141
1142        let analysis = empty_analysis();
1143        let binding_semantics = HashMap::new();
1144        let closure_captures = HashSet::new();
1145        let mutable_captures = HashSet::new();
1146
1147        let input = StoragePlannerInput {
1148            mir: &mir,
1149            analysis: &analysis,
1150            binding_semantics: &binding_semantics,
1151            closure_captures: &closure_captures,
1152            mutable_captures: &mutable_captures,
1153            had_fallbacks: false,
1154            callee_summaries: None,
1155        };
1156
1157        let plan = plan_storage(&input);
1158        assert_eq!(
1159            plan.slot_classes.get(&SlotId(0)),
1160            Some(&BindingStorageClass::Direct)
1161        );
1162    }
1163
1164    // ── Test: Deferred when had_fallbacks ─────────────────────────────────
1165
1166    #[test]
1167    fn test_fallback_gives_deferred() {
1168        let mir = make_mir(
1169            "test_deferred",
1170            vec![BasicBlock {
1171                id: BasicBlockId(0),
1172                statements: vec![],
1173                terminator: make_terminator(TerminatorKind::Return),
1174            }],
1175            2,
1176        );
1177
1178        let analysis = empty_analysis();
1179        let binding_semantics = HashMap::new();
1180        let closure_captures = HashSet::new();
1181        let mutable_captures = HashSet::new();
1182
1183        let input = StoragePlannerInput {
1184            mir: &mir,
1185            analysis: &analysis,
1186            binding_semantics: &binding_semantics,
1187            closure_captures: &closure_captures,
1188            mutable_captures: &mutable_captures,
1189            had_fallbacks: true,
1190            callee_summaries: None,
1191        };
1192
1193        let plan = plan_storage(&input);
1194        assert_eq!(
1195            plan.slot_classes.get(&SlotId(0)),
1196            Some(&BindingStorageClass::Deferred)
1197        );
1198        assert_eq!(
1199            plan.slot_classes.get(&SlotId(1)),
1200            Some(&BindingStorageClass::Deferred)
1201        );
1202    }
1203
1204    // ── Test: Mutable capture → LocalMutablePtr (Phase D) or UniqueHeap ───
1205    //
1206    // Before Phase D this scenario produced `UniqueHeap` unconditionally
1207    // (Rule 2 in `decide_slot_storage`). Phase D adds a post-processing pass:
1208    // when the closure itself is non-escaping, the outer slot is demoted to
1209    // `LocalMutablePtr` so the slot can stay on the stack and the closure env
1210    // holds a typed pointer instead of an `Arc<RwLock<ValueWord>>` cell.
1211
1212    #[test]
1213    fn test_mutable_capture_gets_local_mutable_ptr() {
1214        // bb0: _0 = 0; ClosureCapture(copy _0); _0 = 1; return
1215        let mir = make_mir(
1216            "test_unique_heap",
1217            vec![BasicBlock {
1218                id: BasicBlockId(0),
1219                statements: vec![
1220                    make_stmt(
1221                        StatementKind::Assign(
1222                            Place::Local(SlotId(0)),
1223                            Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
1224                        ),
1225                        0,
1226                    ),
1227                    make_stmt(
1228                        StatementKind::ClosureCapture {
1229                            closure_slot: SlotId(0),
1230                            operands: vec![Operand::Copy(Place::Local(SlotId(0)))],
1231                            function_id: None,
1232                        },
1233                        1,
1234                    ),
1235                    make_stmt(
1236                        StatementKind::Assign(
1237                            Place::Local(SlotId(0)),
1238                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
1239                        ),
1240                        2,
1241                    ),
1242                ],
1243                terminator: make_terminator(TerminatorKind::Return),
1244            }],
1245            1,
1246        );
1247
1248        let analysis = empty_analysis();
1249        let binding_semantics = HashMap::new();
1250
1251        // Simulate what collect_closure_captures would find
1252        let mut closure_captures = HashSet::new();
1253        closure_captures.insert(SlotId(0));
1254        let mut mutable_captures = HashSet::new();
1255        mutable_captures.insert(SlotId(0));
1256
1257        let input = StoragePlannerInput {
1258            mir: &mir,
1259            analysis: &analysis,
1260            binding_semantics: &binding_semantics,
1261            closure_captures: &closure_captures,
1262            mutable_captures: &mutable_captures,
1263            had_fallbacks: false,
1264            callee_summaries: None,
1265        };
1266
1267        let plan = plan_storage(&input);
1268        // Phase D: when the closure is non-escaping, mutable-capture slots
1269        // drop from `UniqueHeap` to `LocalMutablePtr`. The test MIR has the
1270        // closure_slot at SlotId(0) and no escape vectors, so the closure is
1271        // non-escaping and the demotion fires.
1272        assert_eq!(
1273            plan.slot_classes.get(&SlotId(0)),
1274            Some(&BindingStorageClass::LocalMutablePtr)
1275        );
1276    }
1277
1278    // ── Test: SharedCow for aliased+mutated var binding ──────────────────
1279
1280    #[test]
1281    fn test_aliased_mutated_var_gets_shared_cow() {
1282        // bb0: _0 = "hello"; _1 = copy _0; _2 = copy _0; _0 = "world"; return
1283        let mir = make_mir(
1284            "test_shared_cow",
1285            vec![BasicBlock {
1286                id: BasicBlockId(0),
1287                statements: vec![
1288                    make_stmt(
1289                        StatementKind::Assign(
1290                            Place::Local(SlotId(0)),
1291                            Rvalue::Use(Operand::Constant(MirConstant::StringId(0))),
1292                        ),
1293                        0,
1294                    ),
1295                    make_stmt(
1296                        StatementKind::Assign(
1297                            Place::Local(SlotId(1)),
1298                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
1299                        ),
1300                        1,
1301                    ),
1302                    make_stmt(
1303                        StatementKind::Assign(
1304                            Place::Local(SlotId(2)),
1305                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
1306                        ),
1307                        2,
1308                    ),
1309                    make_stmt(
1310                        StatementKind::Assign(
1311                            Place::Local(SlotId(0)),
1312                            Rvalue::Use(Operand::Constant(MirConstant::StringId(1))),
1313                        ),
1314                        3,
1315                    ),
1316                ],
1317                terminator: make_terminator(TerminatorKind::Return),
1318            }],
1319            3,
1320        );
1321
1322        let analysis = empty_analysis();
1323        let mut binding_semantics = HashMap::new();
1324        // Mark slot 0 as a `var` (Flexible) binding
1325        binding_semantics.insert(
1326            0u16,
1327            BindingSemantics::deferred(BindingOwnershipClass::Flexible),
1328        );
1329
1330        let closure_captures = HashSet::new();
1331        let mutable_captures = HashSet::new();
1332
1333        let input = StoragePlannerInput {
1334            mir: &mir,
1335            analysis: &analysis,
1336            binding_semantics: &binding_semantics,
1337            closure_captures: &closure_captures,
1338            mutable_captures: &mutable_captures,
1339            had_fallbacks: false,
1340            callee_summaries: None,
1341        };
1342
1343        let plan = plan_storage(&input);
1344        assert_eq!(
1345            plan.slot_classes.get(&SlotId(0)),
1346            Some(&BindingStorageClass::SharedCow),
1347            "aliased + mutated + Flexible => SharedCow"
1348        );
1349    }
1350
1351    // ── Test: Reference for borrow-holding slot ──────────────────────────
1352
1353    #[test]
1354    fn test_borrow_holder_gets_reference() {
1355        // bb0: _0 = 42; _1 = &_0; return
1356        let mir = make_mir(
1357            "test_reference",
1358            vec![BasicBlock {
1359                id: BasicBlockId(0),
1360                statements: vec![
1361                    make_stmt(
1362                        StatementKind::Assign(
1363                            Place::Local(SlotId(0)),
1364                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1365                        ),
1366                        0,
1367                    ),
1368                    make_stmt(
1369                        StatementKind::Assign(
1370                            Place::Local(SlotId(1)),
1371                            Rvalue::Borrow(BorrowKind::Shared, Place::Local(SlotId(0))),
1372                        ),
1373                        1,
1374                    ),
1375                ],
1376                terminator: make_terminator(TerminatorKind::Return),
1377            }],
1378            2,
1379        );
1380
1381        // Create analysis with a loan on slot 0
1382        let mut analysis = empty_analysis();
1383        analysis.loans.insert(
1384            LoanId(0),
1385            crate::mir::analysis::LoanInfo {
1386                id: LoanId(0),
1387                borrowed_place: Place::Local(SlotId(0)),
1388                kind: BorrowKind::Shared,
1389                issued_at: Point(1),
1390                span: span(),
1391                region_depth: 1,
1392            },
1393        );
1394
1395        let binding_semantics = HashMap::new();
1396        let closure_captures = HashSet::new();
1397        let mutable_captures = HashSet::new();
1398
1399        let input = StoragePlannerInput {
1400            mir: &mir,
1401            analysis: &analysis,
1402            binding_semantics: &binding_semantics,
1403            closure_captures: &closure_captures,
1404            mutable_captures: &mutable_captures,
1405            had_fallbacks: false,
1406            callee_summaries: None,
1407        };
1408
1409        let plan = plan_storage(&input);
1410        // _1 holds a borrow rvalue → Reference
1411        assert_eq!(
1412            plan.slot_classes.get(&SlotId(1)),
1413            Some(&BindingStorageClass::Reference),
1414            "_1 holds &_0 borrow → Reference"
1415        );
1416    }
1417
1418    // ── Test: Explicit Reference preserved ───────────────────────────────
1419
1420    #[test]
1421    fn test_explicit_reference_preserved() {
1422        let mir = make_mir(
1423            "test_explicit_ref",
1424            vec![BasicBlock {
1425                id: BasicBlockId(0),
1426                statements: vec![],
1427                terminator: make_terminator(TerminatorKind::Return),
1428            }],
1429            1,
1430        );
1431
1432        let analysis = empty_analysis();
1433        let mut binding_semantics = HashMap::new();
1434        binding_semantics.insert(
1435            0u16,
1436            BindingSemantics {
1437                ownership_class: BindingOwnershipClass::OwnedImmutable,
1438                storage_class: BindingStorageClass::Reference,
1439                aliasability: Aliasability::Unique,
1440                mutation_capability: MutationCapability::Immutable,
1441                escape_status: EscapeStatus::Local,
1442                return_ownership_hint: None,
1443            },
1444        );
1445
1446        let closure_captures = HashSet::new();
1447        let mutable_captures = HashSet::new();
1448
1449        let input = StoragePlannerInput {
1450            mir: &mir,
1451            analysis: &analysis,
1452            binding_semantics: &binding_semantics,
1453            closure_captures: &closure_captures,
1454            mutable_captures: &mutable_captures,
1455            had_fallbacks: false,
1456            callee_summaries: None,
1457        };
1458
1459        let plan = plan_storage(&input);
1460        assert_eq!(
1461            plan.slot_classes.get(&SlotId(0)),
1462            Some(&BindingStorageClass::Reference),
1463            "explicit Reference annotation preserved"
1464        );
1465    }
1466
1467    // ── Test: collect_closure_captures ────────────────────────────────────
1468
1469    #[test]
1470    fn test_collect_closure_captures() {
1471        // bb0: _0 = 1; _1 = 2; ClosureCapture(copy _0, copy _1); _0 = 3; return
1472        let mir = make_mir(
1473            "test_collect",
1474            vec![BasicBlock {
1475                id: BasicBlockId(0),
1476                statements: vec![
1477                    make_stmt(
1478                        StatementKind::Assign(
1479                            Place::Local(SlotId(0)),
1480                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
1481                        ),
1482                        0,
1483                    ),
1484                    make_stmt(
1485                        StatementKind::Assign(
1486                            Place::Local(SlotId(1)),
1487                            Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
1488                        ),
1489                        1,
1490                    ),
1491                    make_stmt(
1492                        StatementKind::ClosureCapture {
1493                            closure_slot: SlotId(2),
1494                            operands: vec![
1495                                Operand::Copy(Place::Local(SlotId(0))),
1496                                Operand::Copy(Place::Local(SlotId(1))),
1497                            ],
1498                            function_id: None,
1499                        },
1500                        2,
1501                    ),
1502                    make_stmt(
1503                        StatementKind::Assign(
1504                            Place::Local(SlotId(0)),
1505                            Rvalue::Use(Operand::Constant(MirConstant::Int(3))),
1506                        ),
1507                        3,
1508                    ),
1509                ],
1510                terminator: make_terminator(TerminatorKind::Return),
1511            }],
1512            2,
1513        );
1514
1515        let (captures, mutable) = collect_closure_captures(&mir);
1516        assert!(captures.contains(&SlotId(0)));
1517        assert!(captures.contains(&SlotId(1)));
1518        // _0 is assigned twice (before and after capture) → mutably captured
1519        assert!(mutable.contains(&SlotId(0)));
1520        // _1 is assigned only once (initial definition) → not mutably captured
1521        // Note: our conservative check counts any assignment, but _1 only has one
1522        assert!(!mutable.contains(&SlotId(1)));
1523    }
1524
1525    // ── Test: Immutable captured slot stays Direct ───────────────────────
1526
1527    #[test]
1528    fn test_immutable_capture_stays_direct() {
1529        // bb0: _0 = 1; ClosureCapture(copy _0); return
1530        let mir = make_mir(
1531            "test_immutable_capture",
1532            vec![BasicBlock {
1533                id: BasicBlockId(0),
1534                statements: vec![
1535                    make_stmt(
1536                        StatementKind::Assign(
1537                            Place::Local(SlotId(0)),
1538                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
1539                        ),
1540                        0,
1541                    ),
1542                    make_stmt(
1543                        StatementKind::ClosureCapture {
1544                            closure_slot: SlotId(0),
1545                            operands: vec![Operand::Copy(Place::Local(SlotId(0)))],
1546                            function_id: None,
1547                        },
1548                        1,
1549                    ),
1550                ],
1551                terminator: make_terminator(TerminatorKind::Return),
1552            }],
1553            1,
1554        );
1555
1556        let analysis = empty_analysis();
1557        let binding_semantics = HashMap::new();
1558        let mut closure_captures = HashSet::new();
1559        closure_captures.insert(SlotId(0));
1560        let mutable_captures = HashSet::new();
1561
1562        let input = StoragePlannerInput {
1563            mir: &mir,
1564            analysis: &analysis,
1565            binding_semantics: &binding_semantics,
1566            closure_captures: &closure_captures,
1567            mutable_captures: &mutable_captures,
1568            had_fallbacks: false,
1569            callee_summaries: None,
1570        };
1571
1572        let plan = plan_storage(&input);
1573        // Phase D: when a non-escaping closure captures the slot, the planner
1574        // promotes `Direct` → `LocalMutablePtr` unconditionally (the compiler
1575        // side decides whether to actually emit the typed-pointer opcodes
1576        // based on the closure body's mutation analysis). `LocalMutablePtr`
1577        // is semantically equivalent to `Direct` for immutable captures:
1578        // the slot stays on the frame and no heap indirection is introduced.
1579        let class = plan.slot_classes.get(&SlotId(0)).copied();
1580        assert!(
1581            matches!(
1582                class,
1583                Some(BindingStorageClass::Direct)
1584                    | Some(BindingStorageClass::LocalMutablePtr)
1585            ),
1586            "immutable capture stays on stack (Direct or LocalMutablePtr), got {:?}",
1587            class
1588        );
1589    }
1590
1591    // ── Test: Non-Flexible ownership doesn't get SharedCow ───────────────
1592
1593    #[test]
1594    fn test_owned_mutable_aliased_mutated_stays_direct() {
1595        // A `let mut` binding that is aliased and mutated does NOT get
1596        // SharedCow — only `var` (Flexible) does.
1597        let mir = make_mir(
1598            "test_let_mut_no_cow",
1599            vec![BasicBlock {
1600                id: BasicBlockId(0),
1601                statements: vec![
1602                    make_stmt(
1603                        StatementKind::Assign(
1604                            Place::Local(SlotId(0)),
1605                            Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
1606                        ),
1607                        0,
1608                    ),
1609                    make_stmt(
1610                        StatementKind::Assign(
1611                            Place::Local(SlotId(1)),
1612                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
1613                        ),
1614                        1,
1615                    ),
1616                    make_stmt(
1617                        StatementKind::Assign(
1618                            Place::Local(SlotId(2)),
1619                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(0)))),
1620                        ),
1621                        2,
1622                    ),
1623                    make_stmt(
1624                        StatementKind::Assign(
1625                            Place::Local(SlotId(0)),
1626                            Rvalue::Use(Operand::Constant(MirConstant::Int(99))),
1627                        ),
1628                        3,
1629                    ),
1630                ],
1631                terminator: make_terminator(TerminatorKind::Return),
1632            }],
1633            3,
1634        );
1635
1636        let analysis = empty_analysis();
1637        let mut binding_semantics = HashMap::new();
1638        binding_semantics.insert(
1639            0u16,
1640            BindingSemantics::deferred(BindingOwnershipClass::OwnedMutable),
1641        );
1642
1643        let closure_captures = HashSet::new();
1644        let mutable_captures = HashSet::new();
1645
1646        let input = StoragePlannerInput {
1647            mir: &mir,
1648            analysis: &analysis,
1649            binding_semantics: &binding_semantics,
1650            closure_captures: &closure_captures,
1651            mutable_captures: &mutable_captures,
1652            had_fallbacks: false,
1653            callee_summaries: None,
1654        };
1655
1656        let plan = plan_storage(&input);
1657        assert_eq!(
1658            plan.slot_classes.get(&SlotId(0)),
1659            Some(&BindingStorageClass::Direct),
1660            "OwnedMutable (let mut) stays Direct even when aliased+mutated"
1661        );
1662    }
1663
1664    // ── Test: All slots planned ──────────────────────────────────────────
1665
1666    #[test]
1667    fn test_all_slots_planned() {
1668        let mir = make_mir(
1669            "test_all_planned",
1670            vec![BasicBlock {
1671                id: BasicBlockId(0),
1672                statements: vec![],
1673                terminator: make_terminator(TerminatorKind::Return),
1674            }],
1675            5,
1676        );
1677
1678        let analysis = empty_analysis();
1679        let binding_semantics = HashMap::new();
1680        let closure_captures = HashSet::new();
1681        let mutable_captures = HashSet::new();
1682
1683        let input = StoragePlannerInput {
1684            mir: &mir,
1685            analysis: &analysis,
1686            binding_semantics: &binding_semantics,
1687            closure_captures: &closure_captures,
1688            mutable_captures: &mutable_captures,
1689            had_fallbacks: false,
1690            callee_summaries: None,
1691        };
1692
1693        let plan = plan_storage(&input);
1694        assert_eq!(plan.slot_classes.len(), 5, "all slots must be planned");
1695        for i in 0..5 {
1696            assert!(
1697                plan.slot_classes.contains_key(&SlotId(i)),
1698                "slot {} must be in plan",
1699                i
1700            );
1701        }
1702    }
1703
1704    // ── Test: UniqueHeap takes priority over SharedCow ───────────────────
1705
1706    #[test]
1707    fn test_mutable_capture_beats_shared_cow() {
1708        // A `var` binding that is both mutably captured AND aliased+mutated
1709        // should get UniqueHeap (closure mutation takes priority over COW).
1710        let mir = make_mir(
1711            "test_priority",
1712            vec![BasicBlock {
1713                id: BasicBlockId(0),
1714                statements: vec![
1715                    make_stmt(
1716                        StatementKind::Assign(
1717                            Place::Local(SlotId(0)),
1718                            Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
1719                        ),
1720                        0,
1721                    ),
1722                    make_stmt(
1723                        StatementKind::ClosureCapture {
1724                            closure_slot: SlotId(0),
1725                            operands: vec![Operand::Copy(Place::Local(SlotId(0)))],
1726                            function_id: None,
1727                        },
1728                        1,
1729                    ),
1730                    make_stmt(
1731                        StatementKind::Assign(
1732                            Place::Local(SlotId(0)),
1733                            Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
1734                        ),
1735                        2,
1736                    ),
1737                ],
1738                terminator: make_terminator(TerminatorKind::Return),
1739            }],
1740            1,
1741        );
1742
1743        let analysis = empty_analysis();
1744        let mut binding_semantics = HashMap::new();
1745        binding_semantics.insert(
1746            0u16,
1747            BindingSemantics::deferred(BindingOwnershipClass::Flexible),
1748        );
1749
1750        let mut closure_captures = HashSet::new();
1751        closure_captures.insert(SlotId(0));
1752        let mut mutable_captures = HashSet::new();
1753        mutable_captures.insert(SlotId(0));
1754
1755        let input = StoragePlannerInput {
1756            mir: &mir,
1757            analysis: &analysis,
1758            binding_semantics: &binding_semantics,
1759            closure_captures: &closure_captures,
1760            mutable_captures: &mutable_captures,
1761            had_fallbacks: false,
1762            callee_summaries: None,
1763        };
1764
1765        // Phase V0.a changes the storage class depending on the
1766        // `SHAPE_V2_VAR_SHAREDCOW` flag state observed at process start:
1767        //
1768        //   * Flag ON (default): Rule 1b fires first and promotes the `var`
1769        //     slot to `SharedCow`; Phase D's promotion leaves `SharedCow`
1770        //     untouched.
1771        //   * Flag OFF: Rule 1b is suppressed; Rule 2 fires → `UniqueHeap`;
1772        //     Phase D then demotes to `LocalMutablePtr` because the closure
1773        //     is non-escaping.
1774        //
1775        // `plan_storage` consults the cached env-backed flag, so we accept
1776        // whichever outcome matches the process's flag state and cross-check
1777        // the other outcome via `decide_slot_storage` directly.
1778        let plan = plan_storage(&input);
1779        let actual = plan.slot_classes.get(&SlotId(0)).copied();
1780        if var_sharedcow_default_enabled() {
1781            assert_eq!(
1782                actual,
1783                Some(BindingStorageClass::SharedCow),
1784                "Phase V0.a flag on: mutably-captured `var` is SharedCow"
1785            );
1786            // Cross-check: with the flag forced off, the legacy
1787            // UniqueHeap → Phase-D-demote path is still exercised.
1788            assert_eq!(
1789                decide_slot_storage(SlotId(0), &input, /* var_sharedcow_enabled */ false).0,
1790                BindingStorageClass::UniqueHeap,
1791                "legacy path (flag off) still assigns UniqueHeap before Phase D demotion"
1792            );
1793        } else {
1794            assert_eq!(
1795                actual,
1796                Some(BindingStorageClass::LocalMutablePtr),
1797                "Phase V0.a flag off: non-escaping mutable capture → LocalMutablePtr (Phase D)"
1798            );
1799        }
1800    }
1801
1802    // ── Test: detect_escape_status ───────────────────────────────────────
1803
1804    #[test]
1805    fn test_escape_status_local() {
1806        // bb0: _1 = 42; return
1807        // _1 never flows to _0 (return slot) → Local
1808        let mir = make_mir(
1809            "test_local_escape",
1810            vec![BasicBlock {
1811                id: BasicBlockId(0),
1812                statements: vec![make_stmt(
1813                    StatementKind::Assign(
1814                        Place::Local(SlotId(1)),
1815                        Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1816                    ),
1817                    0,
1818                )],
1819                terminator: make_terminator(TerminatorKind::Return),
1820            }],
1821            2,
1822        );
1823
1824        let captures = HashSet::new();
1825        assert_eq!(
1826            detect_escape_status(SlotId(1), &mir, &captures),
1827            EscapeStatus::Local,
1828            "slot that doesn't escape should be Local"
1829        );
1830    }
1831
1832    #[test]
1833    fn test_escape_status_escaped_via_return() {
1834        // bb0: _1 = 42; _0 = copy _1; return
1835        // _1 flows to return slot _0 → Escaped
1836        let mir = make_mir(
1837            "test_escaped",
1838            vec![BasicBlock {
1839                id: BasicBlockId(0),
1840                statements: vec![
1841                    make_stmt(
1842                        StatementKind::Assign(
1843                            Place::Local(SlotId(1)),
1844                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1845                        ),
1846                        0,
1847                    ),
1848                    make_stmt(
1849                        StatementKind::Assign(
1850                            Place::Local(SlotId(0)),
1851                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
1852                        ),
1853                        1,
1854                    ),
1855                ],
1856                terminator: make_terminator(TerminatorKind::Return),
1857            }],
1858            2,
1859        );
1860
1861        let captures = HashSet::new();
1862        assert_eq!(
1863            detect_escape_status(SlotId(1), &mir, &captures),
1864            EscapeStatus::Escaped,
1865            "slot assigned to return slot should be Escaped"
1866        );
1867    }
1868
1869    #[test]
1870    fn test_escape_status_escaped_via_local_alias_chain() {
1871        // bb0: _2 = 42; _1 = copy _2; _0 = copy _1; return
1872        // _2 reaches the return slot transitively through _1.
1873        let mir = make_mir(
1874            "test_transitive_escape",
1875            vec![BasicBlock {
1876                id: BasicBlockId(0),
1877                statements: vec![
1878                    make_stmt(
1879                        StatementKind::Assign(
1880                            Place::Local(SlotId(2)),
1881                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1882                        ),
1883                        0,
1884                    ),
1885                    make_stmt(
1886                        StatementKind::Assign(
1887                            Place::Local(SlotId(1)),
1888                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(2)))),
1889                        ),
1890                        1,
1891                    ),
1892                    make_stmt(
1893                        StatementKind::Assign(
1894                            Place::Local(SlotId(0)),
1895                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
1896                        ),
1897                        2,
1898                    ),
1899                ],
1900                terminator: make_terminator(TerminatorKind::Return),
1901            }],
1902            3,
1903        );
1904
1905        let captures = HashSet::new();
1906        assert_eq!(
1907            detect_escape_status(SlotId(2), &mir, &captures),
1908            EscapeStatus::Escaped,
1909            "slot flowing into a returned local alias should be Escaped"
1910        );
1911    }
1912
1913    #[test]
1914    fn test_escape_status_captured() {
1915        // bb0: _1 = 42; ClosureCapture(copy _1); return
1916        let mir = make_mir(
1917            "test_captured",
1918            vec![BasicBlock {
1919                id: BasicBlockId(0),
1920                statements: vec![
1921                    make_stmt(
1922                        StatementKind::Assign(
1923                            Place::Local(SlotId(1)),
1924                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1925                        ),
1926                        0,
1927                    ),
1928                    make_stmt(
1929                        StatementKind::ClosureCapture {
1930                            closure_slot: SlotId(1),
1931                            operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
1932                            function_id: None,
1933                        },
1934                        1,
1935                    ),
1936                ],
1937                terminator: make_terminator(TerminatorKind::Return),
1938            }],
1939            2,
1940        );
1941
1942        let mut captures = HashSet::new();
1943        captures.insert(SlotId(1));
1944        assert_eq!(
1945            detect_escape_status(SlotId(1), &mir, &captures),
1946            EscapeStatus::Captured,
1947            "slot captured by closure should be Captured"
1948        );
1949    }
1950
1951    #[test]
1952    fn test_escape_status_escaped_beats_captured() {
1953        // A slot that both escapes to return AND is captured → Escaped takes priority
1954        // bb0: _1 = 42; ClosureCapture(copy _1); _0 = copy _1; return
1955        let mir = make_mir(
1956            "test_escaped_captured",
1957            vec![BasicBlock {
1958                id: BasicBlockId(0),
1959                statements: vec![
1960                    make_stmt(
1961                        StatementKind::Assign(
1962                            Place::Local(SlotId(1)),
1963                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
1964                        ),
1965                        0,
1966                    ),
1967                    make_stmt(
1968                        StatementKind::ClosureCapture {
1969                            closure_slot: SlotId(1),
1970                            operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
1971                            function_id: None,
1972                        },
1973                        1,
1974                    ),
1975                    make_stmt(
1976                        StatementKind::Assign(
1977                            Place::Local(SlotId(0)),
1978                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
1979                        ),
1980                        2,
1981                    ),
1982                ],
1983                terminator: make_terminator(TerminatorKind::Return),
1984            }],
1985            2,
1986        );
1987
1988        let mut captures = HashSet::new();
1989        captures.insert(SlotId(1));
1990        assert_eq!(
1991            detect_escape_status(SlotId(1), &mir, &captures),
1992            EscapeStatus::Escaped,
1993            "Escaped takes priority over Captured"
1994        );
1995    }
1996
1997    #[test]
1998    fn test_escape_semantics_in_plan() {
1999        // Verify that the storage plan captures Escaped status on semantics
2000        // bb0: _1 = 42; _0 = copy _1; return
2001        let mir = make_mir(
2002            "test_escape_in_plan",
2003            vec![BasicBlock {
2004                id: BasicBlockId(0),
2005                statements: vec![
2006                    make_stmt(
2007                        StatementKind::Assign(
2008                            Place::Local(SlotId(1)),
2009                            Rvalue::Use(Operand::Constant(MirConstant::Int(42))),
2010                        ),
2011                        0,
2012                    ),
2013                    make_stmt(
2014                        StatementKind::Assign(
2015                            Place::Local(SlotId(0)),
2016                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2017                        ),
2018                        1,
2019                    ),
2020                ],
2021                terminator: make_terminator(TerminatorKind::Return),
2022            }],
2023            2,
2024        );
2025
2026        let analysis = empty_analysis();
2027        let binding_semantics = HashMap::new();
2028        let closure_captures = HashSet::new();
2029        let mutable_captures = HashSet::new();
2030
2031        let input = StoragePlannerInput {
2032            mir: &mir,
2033            analysis: &analysis,
2034            binding_semantics: &binding_semantics,
2035            closure_captures: &closure_captures,
2036            mutable_captures: &mutable_captures,
2037            had_fallbacks: false,
2038            callee_summaries: None,
2039        };
2040
2041        let plan = plan_storage(&input);
2042        assert_eq!(
2043            plan.slot_semantics.get(&SlotId(1)).map(|s| s.escape_status),
2044            Some(EscapeStatus::Escaped),
2045            "slot flowing to return should have Escaped status in plan"
2046        );
2047    }
2048
2049    // ── Tests: inline-array-candidate detection (Phase D.2 infrastructure) ──
2050
2051    #[test]
2052    fn test_inline_hint_for_small_local_aggregate() {
2053        // bb0: _1 = [1, 2, 3]; return
2054        // _1 is a 3-element aggregate that never flows to _0 → candidate.
2055        let mir = make_mir(
2056            "test_inline_small_local",
2057            vec![BasicBlock {
2058                id: BasicBlockId(0),
2059                statements: vec![make_stmt(
2060                    StatementKind::Assign(
2061                        Place::Local(SlotId(1)),
2062                        Rvalue::Aggregate(vec![
2063                            Operand::Constant(MirConstant::Int(1)),
2064                            Operand::Constant(MirConstant::Int(2)),
2065                            Operand::Constant(MirConstant::Int(3)),
2066                        ]),
2067                    ),
2068                    0,
2069                )],
2070                terminator: make_terminator(TerminatorKind::Return),
2071            }],
2072            2,
2073        );
2074
2075        let analysis = empty_analysis();
2076        let binding_semantics = HashMap::new();
2077        let closure_captures = HashSet::new();
2078        let mutable_captures = HashSet::new();
2079
2080        let input = StoragePlannerInput {
2081            mir: &mir,
2082            analysis: &analysis,
2083            binding_semantics: &binding_semantics,
2084            closure_captures: &closure_captures,
2085            mutable_captures: &mutable_captures,
2086            had_fallbacks: false,
2087            callee_summaries: None,
2088        };
2089
2090        let plan = plan_storage(&input);
2091        assert_eq!(
2092            plan.inline_array_sizes.get(&SlotId(1)),
2093            Some(&3),
2094            "3-element non-escaping aggregate should be hinted"
2095        );
2096    }
2097
2098    #[test]
2099    fn test_no_inline_hint_when_aggregate_escapes() {
2100        // bb0: _1 = [1, 2]; _0 = copy _1; return
2101        // _1 flows to return → escapes, so no hint.
2102        let mir = make_mir(
2103            "test_no_inline_escape",
2104            vec![BasicBlock {
2105                id: BasicBlockId(0),
2106                statements: vec![
2107                    make_stmt(
2108                        StatementKind::Assign(
2109                            Place::Local(SlotId(1)),
2110                            Rvalue::Aggregate(vec![
2111                                Operand::Constant(MirConstant::Int(1)),
2112                                Operand::Constant(MirConstant::Int(2)),
2113                            ]),
2114                        ),
2115                        0,
2116                    ),
2117                    make_stmt(
2118                        StatementKind::Assign(
2119                            Place::Local(SlotId(0)),
2120                            Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2121                        ),
2122                        1,
2123                    ),
2124                ],
2125                terminator: make_terminator(TerminatorKind::Return),
2126            }],
2127            2,
2128        );
2129
2130        let analysis = empty_analysis();
2131        let binding_semantics = HashMap::new();
2132        let closure_captures = HashSet::new();
2133        let mutable_captures = HashSet::new();
2134
2135        let input = StoragePlannerInput {
2136            mir: &mir,
2137            analysis: &analysis,
2138            binding_semantics: &binding_semantics,
2139            closure_captures: &closure_captures,
2140            mutable_captures: &mutable_captures,
2141            had_fallbacks: false,
2142            callee_summaries: None,
2143        };
2144
2145        let plan = plan_storage(&input);
2146        assert!(
2147            !plan.inline_array_sizes.contains_key(&SlotId(1)),
2148            "escaping aggregate must not be hinted"
2149        );
2150    }
2151
2152    #[test]
2153    fn test_no_inline_hint_when_aggregate_too_large() {
2154        // bb0: _1 = [1; 9]; return
2155        // 9 > INLINE_ARRAY_MAX_ELEMENTS (8) → no hint.
2156        let big_ops: Vec<Operand> = (0..9)
2157            .map(|_| Operand::Constant(MirConstant::Int(0)))
2158            .collect();
2159        let mir = make_mir(
2160            "test_no_inline_too_large",
2161            vec![BasicBlock {
2162                id: BasicBlockId(0),
2163                statements: vec![make_stmt(
2164                    StatementKind::Assign(
2165                        Place::Local(SlotId(1)),
2166                        Rvalue::Aggregate(big_ops),
2167                    ),
2168                    0,
2169                )],
2170                terminator: make_terminator(TerminatorKind::Return),
2171            }],
2172            2,
2173        );
2174
2175        let analysis = empty_analysis();
2176        let binding_semantics = HashMap::new();
2177        let closure_captures = HashSet::new();
2178        let mutable_captures = HashSet::new();
2179
2180        let input = StoragePlannerInput {
2181            mir: &mir,
2182            analysis: &analysis,
2183            binding_semantics: &binding_semantics,
2184            closure_captures: &closure_captures,
2185            mutable_captures: &mutable_captures,
2186            had_fallbacks: false,
2187            callee_summaries: None,
2188        };
2189
2190        let plan = plan_storage(&input);
2191        assert!(
2192            !plan.inline_array_sizes.contains_key(&SlotId(1)),
2193            "oversize aggregate must not be hinted"
2194        );
2195    }
2196
2197    #[test]
2198    fn test_no_inline_hint_when_aggregate_captured() {
2199        // bb0: _1 = [1, 2]; ClosureCapture(copy _1); return
2200        let mir = make_mir(
2201            "test_no_inline_captured",
2202            vec![BasicBlock {
2203                id: BasicBlockId(0),
2204                statements: vec![
2205                    make_stmt(
2206                        StatementKind::Assign(
2207                            Place::Local(SlotId(1)),
2208                            Rvalue::Aggregate(vec![
2209                                Operand::Constant(MirConstant::Int(1)),
2210                                Operand::Constant(MirConstant::Int(2)),
2211                            ]),
2212                        ),
2213                        0,
2214                    ),
2215                    make_stmt(
2216                        StatementKind::ClosureCapture {
2217                            closure_slot: SlotId(1),
2218                            operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
2219                            function_id: None,
2220                        },
2221                        1,
2222                    ),
2223                ],
2224                terminator: make_terminator(TerminatorKind::Return),
2225            }],
2226            2,
2227        );
2228
2229        let analysis = empty_analysis();
2230        let binding_semantics = HashMap::new();
2231        let mut closure_captures = HashSet::new();
2232        closure_captures.insert(SlotId(1));
2233        let mutable_captures = HashSet::new();
2234
2235        let input = StoragePlannerInput {
2236            mir: &mir,
2237            analysis: &analysis,
2238            binding_semantics: &binding_semantics,
2239            closure_captures: &closure_captures,
2240            mutable_captures: &mutable_captures,
2241            had_fallbacks: false,
2242            callee_summaries: None,
2243        };
2244
2245        let plan = plan_storage(&input);
2246        assert!(
2247            !plan.inline_array_sizes.contains_key(&SlotId(1)),
2248            "captured aggregate must not be hinted"
2249        );
2250    }
2251
2252    #[test]
2253    fn test_no_inline_hint_when_reassigned() {
2254        // bb0: _1 = [1, 2]; _1 = [3, 4]; return
2255        // Two aggregate assignments to the same slot → disqualify.
2256        let mir = make_mir(
2257            "test_no_inline_reassigned",
2258            vec![BasicBlock {
2259                id: BasicBlockId(0),
2260                statements: vec![
2261                    make_stmt(
2262                        StatementKind::Assign(
2263                            Place::Local(SlotId(1)),
2264                            Rvalue::Aggregate(vec![
2265                                Operand::Constant(MirConstant::Int(1)),
2266                                Operand::Constant(MirConstant::Int(2)),
2267                            ]),
2268                        ),
2269                        0,
2270                    ),
2271                    make_stmt(
2272                        StatementKind::Assign(
2273                            Place::Local(SlotId(1)),
2274                            Rvalue::Aggregate(vec![
2275                                Operand::Constant(MirConstant::Int(3)),
2276                                Operand::Constant(MirConstant::Int(4)),
2277                            ]),
2278                        ),
2279                        1,
2280                    ),
2281                ],
2282                terminator: make_terminator(TerminatorKind::Return),
2283            }],
2284            2,
2285        );
2286
2287        let analysis = empty_analysis();
2288        let binding_semantics = HashMap::new();
2289        let closure_captures = HashSet::new();
2290        let mutable_captures = HashSet::new();
2291
2292        let input = StoragePlannerInput {
2293            mir: &mir,
2294            analysis: &analysis,
2295            binding_semantics: &binding_semantics,
2296            closure_captures: &closure_captures,
2297            mutable_captures: &mutable_captures,
2298            had_fallbacks: false,
2299            callee_summaries: None,
2300        };
2301
2302        let plan = plan_storage(&input);
2303        assert!(
2304            !plan.inline_array_sizes.contains_key(&SlotId(1)),
2305            "re-assigned slot must not be hinted"
2306        );
2307    }
2308
2309    #[test]
2310    fn test_inline_hint_at_boundary_size() {
2311        // Exactly INLINE_ARRAY_MAX_ELEMENTS elements → eligible.
2312        let ops: Vec<Operand> = (0..INLINE_ARRAY_MAX_ELEMENTS)
2313            .map(|_| Operand::Constant(MirConstant::Int(0)))
2314            .collect();
2315        let mir = make_mir(
2316            "test_inline_boundary",
2317            vec![BasicBlock {
2318                id: BasicBlockId(0),
2319                statements: vec![make_stmt(
2320                    StatementKind::Assign(
2321                        Place::Local(SlotId(1)),
2322                        Rvalue::Aggregate(ops),
2323                    ),
2324                    0,
2325                )],
2326                terminator: make_terminator(TerminatorKind::Return),
2327            }],
2328            2,
2329        );
2330
2331        let analysis = empty_analysis();
2332        let binding_semantics = HashMap::new();
2333        let closure_captures = HashSet::new();
2334        let mutable_captures = HashSet::new();
2335
2336        let input = StoragePlannerInput {
2337            mir: &mir,
2338            analysis: &analysis,
2339            binding_semantics: &binding_semantics,
2340            closure_captures: &closure_captures,
2341            mutable_captures: &mutable_captures,
2342            had_fallbacks: false,
2343            callee_summaries: None,
2344        };
2345
2346        let plan = plan_storage(&input);
2347        assert_eq!(
2348            plan.inline_array_sizes.get(&SlotId(1)),
2349            Some(&INLINE_ARRAY_MAX_ELEMENTS),
2350            "boundary-size aggregate should be hinted"
2351        );
2352    }
2353
2354    #[test]
2355    fn test_no_inline_hint_with_fallbacks() {
2356        // had_fallbacks=true → planner bails to Deferred and records no hints.
2357        let mir = make_mir(
2358            "test_no_inline_fallback",
2359            vec![BasicBlock {
2360                id: BasicBlockId(0),
2361                statements: vec![make_stmt(
2362                    StatementKind::Assign(
2363                        Place::Local(SlotId(1)),
2364                        Rvalue::Aggregate(vec![
2365                            Operand::Constant(MirConstant::Int(1)),
2366                        ]),
2367                    ),
2368                    0,
2369                )],
2370                terminator: make_terminator(TerminatorKind::Return),
2371            }],
2372            2,
2373        );
2374
2375        let analysis = empty_analysis();
2376        let binding_semantics = HashMap::new();
2377        let closure_captures = HashSet::new();
2378        let mutable_captures = HashSet::new();
2379
2380        let input = StoragePlannerInput {
2381            mir: &mir,
2382            analysis: &analysis,
2383            binding_semantics: &binding_semantics,
2384            closure_captures: &closure_captures,
2385            mutable_captures: &mutable_captures,
2386            had_fallbacks: true,
2387            callee_summaries: None,
2388        };
2389
2390        let plan = plan_storage(&input);
2391        assert!(
2392            plan.inline_array_sizes.is_empty(),
2393            "fallback path must not record any hints"
2394        );
2395    }
2396
2397    // ── Closure Spec Phase B: non-escape detection for closure slots ────────
2398    //
2399    // These tests exercise the 10-row escape-vector table from
2400    // `docs/v2-closure-specialization.md` §2.1 plus the §2.4 transitive
2401    // fixed-point. They operate directly on synthetic MIR so they can target
2402    // specific vectors without relying on the higher-level compiler pipeline.
2403    //
2404    // Convention: closure slot := the `closure_slot` field of a
2405    // `StatementKind::ClosureCapture`. A followup
2406    // `Assign(_, ClosurePlaceholder)` would complete the definition in real
2407    // MIR; the tests omit it because `collect_closure_slots` keys solely on
2408    // the `ClosureCapture` statement.
2409    //
2410    // Helper: build MIR with a single block + terminator.
2411    fn single_block_mir(name: &str, statements: Vec<MirStatement>, num_locals: u16) -> MirFunction {
2412        make_mir(
2413            name,
2414            vec![BasicBlock {
2415                id: BasicBlockId(0),
2416                statements,
2417                terminator: make_terminator(TerminatorKind::Return),
2418            }],
2419            num_locals,
2420        )
2421    }
2422
2423    fn run_planner(mir: &MirFunction) -> StoragePlan {
2424        let analysis = empty_analysis();
2425        let binding_semantics = HashMap::new();
2426        let closure_captures = HashSet::new();
2427        let mutable_captures = HashSet::new();
2428        let input = StoragePlannerInput {
2429            mir,
2430            analysis: &analysis,
2431            binding_semantics: &binding_semantics,
2432            closure_captures: &closure_captures,
2433            mutable_captures: &mutable_captures,
2434            had_fallbacks: false,
2435            callee_summaries: None,
2436        };
2437        plan_storage(&input)
2438    }
2439
2440    #[test]
2441    fn test_phase_b_pure_local_closure_is_non_escaping() {
2442        // let f = || 1; f()  — the closure slot is defined via
2443        // ClosureCapture (no captures) and then only invoked. No escape
2444        // vector fires.
2445        // Slots: _0 = return, _1 = closure, _2 = call result
2446        let mir = single_block_mir(
2447            "phase_b_pure_local",
2448            vec![
2449                make_stmt(
2450                    StatementKind::ClosureCapture {
2451                        closure_slot: SlotId(1),
2452                        operands: vec![],
2453                        function_id: None,
2454                    },
2455                    0,
2456                ),
2457                make_stmt(
2458                    StatementKind::Assign(
2459                        Place::Local(SlotId(1)),
2460                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2461                    ),
2462                    1,
2463                ),
2464            ],
2465            3,
2466        );
2467
2468        let plan = run_planner(&mir);
2469        assert!(
2470            plan.non_escaping_closure_slots.contains(&SlotId(1)),
2471            "pure let f = || 1 should be non-escaping; got {:?}",
2472            plan.non_escaping_closure_slots
2473        );
2474    }
2475
2476    #[test]
2477    fn test_phase_b_closure_returned_is_escaping() {
2478        // fn make() { || 42 } — the closure slot flows into the return slot.
2479        // Slots: _0 = return, _1 = closure
2480        let mir = single_block_mir(
2481            "phase_b_returned",
2482            vec![
2483                make_stmt(
2484                    StatementKind::ClosureCapture {
2485                        closure_slot: SlotId(1),
2486                        operands: vec![],
2487                        function_id: None,
2488                    },
2489                    0,
2490                ),
2491                make_stmt(
2492                    StatementKind::Assign(
2493                        Place::Local(SlotId(1)),
2494                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2495                    ),
2496                    1,
2497                ),
2498                make_stmt(
2499                    StatementKind::Assign(
2500                        Place::Local(SlotId(0)),
2501                        Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
2502                    ),
2503                    2,
2504                ),
2505            ],
2506            2,
2507        );
2508
2509        let plan = run_planner(&mir);
2510        assert!(
2511            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
2512            "returned closure must be escaping; got {:?}",
2513            plan.non_escaping_closure_slots
2514        );
2515    }
2516
2517    #[test]
2518    fn test_phase_b_closure_in_array_literal_is_escaping() {
2519        // let a = [|x| x]  — Rvalue::Aggregate row 2.
2520        // Slots: _0 = return, _1 = closure, _2 = array
2521        let mir = single_block_mir(
2522            "phase_b_in_array_literal",
2523            vec![
2524                make_stmt(
2525                    StatementKind::ClosureCapture {
2526                        closure_slot: SlotId(1),
2527                        operands: vec![],
2528                        function_id: None,
2529                    },
2530                    0,
2531                ),
2532                make_stmt(
2533                    StatementKind::Assign(
2534                        Place::Local(SlotId(1)),
2535                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2536                    ),
2537                    1,
2538                ),
2539                make_stmt(
2540                    StatementKind::Assign(
2541                        Place::Local(SlotId(2)),
2542                        Rvalue::Aggregate(vec![Operand::Copy(Place::Local(SlotId(1)))]),
2543                    ),
2544                    2,
2545                ),
2546            ],
2547            3,
2548        );
2549
2550        let plan = run_planner(&mir);
2551        assert!(
2552            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
2553            "closure stored in array literal must be escaping; got {:?}",
2554            plan.non_escaping_closure_slots
2555        );
2556    }
2557
2558    #[test]
2559    fn test_phase_b_closure_pushed_via_array_store_is_escaping() {
2560        // let a = []; a.push(|x| x)  — StatementKind::ArrayStore row 2.
2561        // Slots: _0 = return, _1 = array, _2 = closure
2562        let mir = single_block_mir(
2563            "phase_b_array_store",
2564            vec![
2565                make_stmt(
2566                    StatementKind::Assign(
2567                        Place::Local(SlotId(1)),
2568                        Rvalue::Aggregate(vec![]),
2569                    ),
2570                    0,
2571                ),
2572                make_stmt(
2573                    StatementKind::ClosureCapture {
2574                        closure_slot: SlotId(2),
2575                        operands: vec![],
2576                        function_id: None,
2577                    },
2578                    1,
2579                ),
2580                make_stmt(
2581                    StatementKind::Assign(
2582                        Place::Local(SlotId(2)),
2583                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2584                    ),
2585                    2,
2586                ),
2587                make_stmt(
2588                    StatementKind::ArrayStore {
2589                        container_slot: SlotId(1),
2590                        operands: vec![Operand::Copy(Place::Local(SlotId(2)))],
2591                    },
2592                    3,
2593                ),
2594            ],
2595            3,
2596        );
2597
2598        let plan = run_planner(&mir);
2599        assert!(
2600            !plan.non_escaping_closure_slots.contains(&SlotId(2)),
2601            "closure pushed into array via ArrayStore must be escaping; got {:?}",
2602            plan.non_escaping_closure_slots
2603        );
2604    }
2605
2606    #[test]
2607    fn test_phase_b_closure_stored_in_object_field_is_escaping() {
2608        // foo.f = || 1  — Place::Field write row 3.
2609        // Slots: _0 = return, _1 = foo (struct), _2 = closure
2610        let mir = single_block_mir(
2611            "phase_b_field_store",
2612            vec![
2613                make_stmt(
2614                    StatementKind::ClosureCapture {
2615                        closure_slot: SlotId(2),
2616                        operands: vec![],
2617                        function_id: None,
2618                    },
2619                    0,
2620                ),
2621                make_stmt(
2622                    StatementKind::Assign(
2623                        Place::Local(SlotId(2)),
2624                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2625                    ),
2626                    1,
2627                ),
2628                make_stmt(
2629                    StatementKind::Assign(
2630                        Place::Field(Box::new(Place::Local(SlotId(1))), FieldIdx(0)),
2631                        Rvalue::Use(Operand::Copy(Place::Local(SlotId(2)))),
2632                    ),
2633                    2,
2634                ),
2635            ],
2636            3,
2637        );
2638
2639        let plan = run_planner(&mir);
2640        assert!(
2641            !plan.non_escaping_closure_slots.contains(&SlotId(2)),
2642            "closure stored in struct field must be escaping; got {:?}",
2643            plan.non_escaping_closure_slots
2644        );
2645    }
2646
2647    #[test]
2648    fn test_phase_b_closure_across_detached_task_is_escaping() {
2649        // async scope { || 1 } — TaskBoundary(Detached) row 5.
2650        let mir = single_block_mir(
2651            "phase_b_detached_task",
2652            vec![
2653                make_stmt(
2654                    StatementKind::ClosureCapture {
2655                        closure_slot: SlotId(1),
2656                        operands: vec![],
2657                        function_id: None,
2658                    },
2659                    0,
2660                ),
2661                make_stmt(
2662                    StatementKind::Assign(
2663                        Place::Local(SlotId(1)),
2664                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2665                    ),
2666                    1,
2667                ),
2668                make_stmt(
2669                    StatementKind::TaskBoundary(
2670                        vec![Operand::Copy(Place::Local(SlotId(1)))],
2671                        TaskBoundaryKind::Detached,
2672                    ),
2673                    2,
2674                ),
2675            ],
2676            2,
2677        );
2678
2679        let plan = run_planner(&mir);
2680        assert!(
2681            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
2682            "closure crossing detached task boundary must be escaping; got {:?}",
2683            plan.non_escaping_closure_slots
2684        );
2685    }
2686
2687    #[test]
2688    fn test_phase_b_closure_across_structured_task_is_escaping() {
2689        // Structured task boundaries conservatively escape in Phase B per
2690        // §5.5 — stack allocation through structured boundaries is deferred.
2691        let mir = single_block_mir(
2692            "phase_b_structured_task",
2693            vec![
2694                make_stmt(
2695                    StatementKind::ClosureCapture {
2696                        closure_slot: SlotId(1),
2697                        operands: vec![],
2698                        function_id: None,
2699                    },
2700                    0,
2701                ),
2702                make_stmt(
2703                    StatementKind::Assign(
2704                        Place::Local(SlotId(1)),
2705                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2706                    ),
2707                    1,
2708                ),
2709                make_stmt(
2710                    StatementKind::TaskBoundary(
2711                        vec![Operand::Copy(Place::Local(SlotId(1)))],
2712                        TaskBoundaryKind::Structured,
2713                    ),
2714                    2,
2715                ),
2716            ],
2717            2,
2718        );
2719
2720        let plan = run_planner(&mir);
2721        assert!(
2722            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
2723            "closure crossing structured task boundary must be escaping (conservative); got {:?}",
2724            plan.non_escaping_closure_slots
2725        );
2726    }
2727
2728    #[test]
2729    fn test_phase_b_closure_written_through_deref_is_escaping() {
2730        // *p = |x| x — Place::Deref write row 9.
2731        // Slots: _0 = return, _1 = pointer-holding slot, _2 = closure
2732        let mir = single_block_mir(
2733            "phase_b_deref_write",
2734            vec![
2735                make_stmt(
2736                    StatementKind::ClosureCapture {
2737                        closure_slot: SlotId(2),
2738                        operands: vec![],
2739                        function_id: None,
2740                    },
2741                    0,
2742                ),
2743                make_stmt(
2744                    StatementKind::Assign(
2745                        Place::Local(SlotId(2)),
2746                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2747                    ),
2748                    1,
2749                ),
2750                make_stmt(
2751                    StatementKind::Assign(
2752                        Place::Deref(Box::new(Place::Local(SlotId(1)))),
2753                        Rvalue::Use(Operand::Copy(Place::Local(SlotId(2)))),
2754                    ),
2755                    2,
2756                ),
2757            ],
2758            3,
2759        );
2760
2761        let plan = run_planner(&mir);
2762        assert!(
2763            !plan.non_escaping_closure_slots.contains(&SlotId(2)),
2764            "closure written through deref must be escaping; got {:?}",
2765            plan.non_escaping_closure_slots
2766        );
2767    }
2768
2769    #[test]
2770    fn test_phase_b_closure_promoted_to_shared_cow_is_escaping() {
2771        // Row 10: if the semantics planner assigns the closure slot
2772        // SharedCow/UniqueHeap storage, the closure value is escaping by
2773        // construction.
2774        let mir = single_block_mir(
2775            "phase_b_shared_cow_promotion",
2776            vec![
2777                make_stmt(
2778                    StatementKind::ClosureCapture {
2779                        closure_slot: SlotId(1),
2780                        operands: vec![],
2781                        function_id: None,
2782                    },
2783                    0,
2784                ),
2785                make_stmt(
2786                    StatementKind::Assign(
2787                        Place::Local(SlotId(1)),
2788                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2789                    ),
2790                    1,
2791                ),
2792            ],
2793            2,
2794        );
2795
2796        let analysis = empty_analysis();
2797        let mut binding_semantics = HashMap::new();
2798        binding_semantics.insert(
2799            1u16,
2800            BindingSemantics {
2801                ownership_class: BindingOwnershipClass::Flexible,
2802                storage_class: BindingStorageClass::SharedCow,
2803                aliasability: Aliasability::SharedMutable,
2804                mutation_capability: MutationCapability::SharedMutable,
2805                escape_status: EscapeStatus::Local,
2806                return_ownership_hint: None,
2807            },
2808        );
2809        let closure_captures = HashSet::new();
2810        let mutable_captures = HashSet::new();
2811        let input = StoragePlannerInput {
2812            mir: &mir,
2813            analysis: &analysis,
2814            binding_semantics: &binding_semantics,
2815            closure_captures: &closure_captures,
2816            mutable_captures: &mutable_captures,
2817            had_fallbacks: false,
2818            callee_summaries: None,
2819        };
2820        let plan = plan_storage(&input);
2821        assert!(
2822            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
2823            "SharedCow-promoted closure slot must be escaping; got {:?}",
2824            plan.non_escaping_closure_slots
2825        );
2826    }
2827
2828    #[test]
2829    fn test_phase_b_transitive_closure_capture_escapes_together() {
2830        // let f = |x| x; let g = |y| f(y) + y — g captures f. If g escapes
2831        // (returned), f must also be classified as escaping. Row 4 +
2832        // transitive fixed-point (§2.4).
2833        // Slots: _0 = return, _1 = f, _2 = g
2834        let mir = single_block_mir(
2835            "phase_b_transitive_escape",
2836            vec![
2837                // Define f
2838                make_stmt(
2839                    StatementKind::ClosureCapture {
2840                        closure_slot: SlotId(1),
2841                        operands: vec![],
2842                        function_id: None,
2843                    },
2844                    0,
2845                ),
2846                make_stmt(
2847                    StatementKind::Assign(
2848                        Place::Local(SlotId(1)),
2849                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2850                    ),
2851                    1,
2852                ),
2853                // Define g, capturing f
2854                make_stmt(
2855                    StatementKind::ClosureCapture {
2856                        closure_slot: SlotId(2),
2857                        operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
2858                        function_id: None,
2859                    },
2860                    2,
2861                ),
2862                make_stmt(
2863                    StatementKind::Assign(
2864                        Place::Local(SlotId(2)),
2865                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2866                    ),
2867                    3,
2868                ),
2869                // g flows to the return slot — g escapes.
2870                make_stmt(
2871                    StatementKind::Assign(
2872                        Place::Local(SlotId(0)),
2873                        Rvalue::Use(Operand::Copy(Place::Local(SlotId(2)))),
2874                    ),
2875                    4,
2876                ),
2877            ],
2878            3,
2879        );
2880
2881        let plan = run_planner(&mir);
2882        assert!(
2883            !plan.non_escaping_closure_slots.contains(&SlotId(2)),
2884            "escaping g must not be classified non-escaping"
2885        );
2886        assert!(
2887            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
2888            "f is captured by escaping g → f must also escape (§2.4); got {:?}",
2889            plan.non_escaping_closure_slots
2890        );
2891    }
2892
2893    #[test]
2894    fn test_phase_b_transitive_capture_both_non_escaping() {
2895        // Dual of the test above — when g is non-escaping, the closure it
2896        // captures (f) should also be non-escaping. Proves the fixed-point
2897        // does not over-mark as escaping.
2898        // Slots: _0 = return, _1 = f, _2 = g
2899        let mir = single_block_mir(
2900            "phase_b_transitive_non_escape",
2901            vec![
2902                make_stmt(
2903                    StatementKind::ClosureCapture {
2904                        closure_slot: SlotId(1),
2905                        operands: vec![],
2906                        function_id: None,
2907                    },
2908                    0,
2909                ),
2910                make_stmt(
2911                    StatementKind::Assign(
2912                        Place::Local(SlotId(1)),
2913                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2914                    ),
2915                    1,
2916                ),
2917                make_stmt(
2918                    StatementKind::ClosureCapture {
2919                        closure_slot: SlotId(2),
2920                        operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
2921                        function_id: None,
2922                    },
2923                    2,
2924                ),
2925                make_stmt(
2926                    StatementKind::Assign(
2927                        Place::Local(SlotId(2)),
2928                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2929                    ),
2930                    3,
2931                ),
2932            ],
2933            3,
2934        );
2935
2936        let plan = run_planner(&mir);
2937        assert!(
2938            plan.non_escaping_closure_slots.contains(&SlotId(1)),
2939            "non-escaping g keeps f non-escaping; got {:?}",
2940            plan.non_escaping_closure_slots
2941        );
2942        assert!(
2943            plan.non_escaping_closure_slots.contains(&SlotId(2)),
2944            "g itself is non-escaping"
2945        );
2946    }
2947
2948    #[test]
2949    fn test_phase_b_call_arg_conservative_without_summary() {
2950        // arr.map(|x| x+1) — without a callee summary, Phase B must
2951        // classify the closure as escaping (sub-task 6 conservative
2952        // fallback). Phase C will refine this via the mono key.
2953        // Slots: _0 = return, _1 = closure, _2 = result, _3 = arr
2954        let mir = make_mir(
2955            "phase_b_call_arg_conservative",
2956            vec![BasicBlock {
2957                id: BasicBlockId(0),
2958                statements: vec![
2959                    make_stmt(
2960                        StatementKind::ClosureCapture {
2961                            closure_slot: SlotId(1),
2962                            operands: vec![],
2963                            function_id: None,
2964                        },
2965                        0,
2966                    ),
2967                    make_stmt(
2968                        StatementKind::Assign(
2969                            Place::Local(SlotId(1)),
2970                            Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
2971                        ),
2972                        1,
2973                    ),
2974                ],
2975                terminator: Terminator {
2976                    kind: TerminatorKind::Call {
2977                        func: Operand::Constant(MirConstant::Function("map".to_string())),
2978                        args: vec![
2979                            Operand::Copy(Place::Local(SlotId(3))),
2980                            Operand::Copy(Place::Local(SlotId(1))),
2981                        ],
2982                        destination: Place::Local(SlotId(2)),
2983                        next: BasicBlockId(0),
2984                    },
2985                    span: span(),
2986                },
2987            }],
2988            4,
2989        );
2990
2991        let plan = run_planner(&mir);
2992        assert!(
2993            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
2994            "closure passed as call arg without a callee summary must be conservative = escaping"
2995        );
2996    }
2997
2998    #[test]
2999    fn test_phase_b_call_arg_with_non_escaping_summary() {
3000        // arr.map(|x| x+1) — with a callee summary marking the
3001        // corresponding parameter as non-escaping, the closure is allowed to
3002        // be classified as non-escaping.
3003        use crate::mir::analysis::{FunctionBorrowSummary, ReturnOwnershipMode};
3004
3005        let mir = make_mir(
3006            "phase_b_call_arg_with_summary",
3007            vec![BasicBlock {
3008                id: BasicBlockId(0),
3009                statements: vec![
3010                    make_stmt(
3011                        StatementKind::ClosureCapture {
3012                            closure_slot: SlotId(1),
3013                            operands: vec![],
3014                            function_id: None,
3015                        },
3016                        0,
3017                    ),
3018                    make_stmt(
3019                        StatementKind::Assign(
3020                            Place::Local(SlotId(1)),
3021                            Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3022                        ),
3023                        1,
3024                    ),
3025                ],
3026                terminator: Terminator {
3027                    kind: TerminatorKind::Call {
3028                        func: Operand::Constant(MirConstant::Function(
3029                            "trusted_non_escaping".to_string(),
3030                        )),
3031                        args: vec![
3032                            Operand::Copy(Place::Local(SlotId(3))),
3033                            Operand::Copy(Place::Local(SlotId(1))),
3034                        ],
3035                        destination: Place::Local(SlotId(2)),
3036                        next: BasicBlockId(0),
3037                    },
3038                    span: span(),
3039                },
3040            }],
3041            4,
3042        );
3043
3044        let analysis = empty_analysis();
3045        let binding_semantics = HashMap::new();
3046        let closure_captures = HashSet::new();
3047        let mutable_captures = HashSet::new();
3048        let mut summaries: HashMap<String, FunctionBorrowSummary> = HashMap::new();
3049        summaries.insert(
3050            "trusted_non_escaping".to_string(),
3051            FunctionBorrowSummary {
3052                param_borrows: vec![None, None],
3053                conflict_pairs: vec![],
3054                return_summary: None,
3055                return_ownership_mode: ReturnOwnershipMode::Unknown,
3056                // Both params non-escaping.
3057                closure_param_escapes: vec![false, false],
3058            },
3059        );
3060
3061        let input = StoragePlannerInput {
3062            mir: &mir,
3063            analysis: &analysis,
3064            binding_semantics: &binding_semantics,
3065            closure_captures: &closure_captures,
3066            mutable_captures: &mutable_captures,
3067            had_fallbacks: false,
3068            callee_summaries: Some(&summaries),
3069        };
3070        let plan = plan_storage(&input);
3071        assert!(
3072            plan.non_escaping_closure_slots.contains(&SlotId(1)),
3073            "closure passed to a callee with a non-escaping param summary is non-escaping; got {:?}",
3074            plan.non_escaping_closure_slots
3075        );
3076    }
3077
3078    #[test]
3079    fn test_phase_b_snapshot_call_forces_escape() {
3080        // Row 7: `snapshot()` is opaque FFI — any closure flowing into it
3081        // must be treated as escaping.
3082        let mir = make_mir(
3083            "phase_b_snapshot_escape",
3084            vec![BasicBlock {
3085                id: BasicBlockId(0),
3086                statements: vec![
3087                    make_stmt(
3088                        StatementKind::ClosureCapture {
3089                            closure_slot: SlotId(1),
3090                            operands: vec![],
3091                            function_id: None,
3092                        },
3093                        0,
3094                    ),
3095                    make_stmt(
3096                        StatementKind::Assign(
3097                            Place::Local(SlotId(1)),
3098                            Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3099                        ),
3100                        1,
3101                    ),
3102                ],
3103                terminator: Terminator {
3104                    kind: TerminatorKind::Call {
3105                        func: Operand::Constant(MirConstant::Function("snapshot".to_string())),
3106                        args: vec![Operand::Copy(Place::Local(SlotId(1)))],
3107                        destination: Place::Local(SlotId(2)),
3108                        next: BasicBlockId(0),
3109                    },
3110                    span: span(),
3111                },
3112            }],
3113            3,
3114        );
3115
3116        let plan = run_planner(&mir);
3117        assert!(
3118            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
3119            "closure fed to snapshot() must be escaping"
3120        );
3121    }
3122
3123    #[test]
3124    fn test_phase_b_enum_store_is_escaping() {
3125        // Rvalue 2 subcase: closure stored into an enum payload.
3126        // Slots: _0 = return, _1 = closure, _2 = enum
3127        let mir = single_block_mir(
3128            "phase_b_enum_store",
3129            vec![
3130                make_stmt(
3131                    StatementKind::ClosureCapture {
3132                        closure_slot: SlotId(1),
3133                        operands: vec![],
3134                        function_id: None,
3135                    },
3136                    0,
3137                ),
3138                make_stmt(
3139                    StatementKind::Assign(
3140                        Place::Local(SlotId(1)),
3141                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3142                    ),
3143                    1,
3144                ),
3145                make_stmt(
3146                    StatementKind::EnumStore {
3147                        container_slot: SlotId(2),
3148                        operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
3149                        variant_name: None,
3150                    },
3151                    2,
3152                ),
3153            ],
3154            3,
3155        );
3156
3157        let plan = run_planner(&mir);
3158        assert!(
3159            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
3160            "closure stored in enum payload must be escaping"
3161        );
3162    }
3163
3164    #[test]
3165    fn test_phase_b_object_store_is_escaping() {
3166        // Rvalue 2 subcase: closure stored into an object/struct literal.
3167        // Slots: _0 = return, _1 = closure, _2 = object
3168        let mir = single_block_mir(
3169            "phase_b_object_store",
3170            vec![
3171                make_stmt(
3172                    StatementKind::ClosureCapture {
3173                        closure_slot: SlotId(1),
3174                        operands: vec![],
3175                        function_id: None,
3176                    },
3177                    0,
3178                ),
3179                make_stmt(
3180                    StatementKind::Assign(
3181                        Place::Local(SlotId(1)),
3182                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3183                    ),
3184                    1,
3185                ),
3186                make_stmt(
3187                    StatementKind::ObjectStore {
3188                        container_slot: SlotId(2),
3189                        operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
3190                        field_names: vec!["f".to_string()],
3191                        schema_id: None,
3192                    },
3193                    2,
3194                ),
3195            ],
3196            3,
3197        );
3198
3199        let plan = run_planner(&mir);
3200        assert!(
3201            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
3202            "closure stored in object literal must be escaping"
3203        );
3204    }
3205
3206    #[test]
3207    fn test_phase_b_two_independent_closures() {
3208        // Two independent closures in the same function, one escapes the
3209        // other does not — verifies the per-slot classification doesn't
3210        // over-report.
3211        // Slots: _0 = return, _1 = escaping, _2 = local, _3 = result of local call
3212        let mir = single_block_mir(
3213            "phase_b_two_independent",
3214            vec![
3215                // First closure: escapes via return.
3216                make_stmt(
3217                    StatementKind::ClosureCapture {
3218                        closure_slot: SlotId(1),
3219                        operands: vec![],
3220                        function_id: None,
3221                    },
3222                    0,
3223                ),
3224                make_stmt(
3225                    StatementKind::Assign(
3226                        Place::Local(SlotId(1)),
3227                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3228                    ),
3229                    1,
3230                ),
3231                // Second closure: never used beyond its assignment.
3232                make_stmt(
3233                    StatementKind::ClosureCapture {
3234                        closure_slot: SlotId(2),
3235                        operands: vec![],
3236                        function_id: None,
3237                    },
3238                    2,
3239                ),
3240                make_stmt(
3241                    StatementKind::Assign(
3242                        Place::Local(SlotId(2)),
3243                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3244                    ),
3245                    3,
3246                ),
3247                // Return the first.
3248                make_stmt(
3249                    StatementKind::Assign(
3250                        Place::Local(SlotId(0)),
3251                        Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
3252                    ),
3253                    4,
3254                ),
3255            ],
3256            3,
3257        );
3258
3259        let plan = run_planner(&mir);
3260        assert!(
3261            !plan.non_escaping_closure_slots.contains(&SlotId(1)),
3262            "first closure escapes via return"
3263        );
3264        assert!(
3265            plan.non_escaping_closure_slots.contains(&SlotId(2)),
3266            "second closure is independent and does not escape; got {:?}",
3267            plan.non_escaping_closure_slots
3268        );
3269    }
3270
3271    // ── Closure Spec Phase D: LocalMutablePtr promotion ──────────────────
3272
3273    /// Build a minimal MIR: outer mutable slot `_1`, closure slot `_2`
3274    /// captures `_1`, closure is invoked. Phase B classifies `_2` as
3275    /// non-escaping; Phase D demotes `_1` from `UniqueHeap` → `LocalMutablePtr`.
3276    fn phase_d_basic_mir() -> MirFunction {
3277        single_block_mir(
3278            "phase_d_local_ptr",
3279            vec![
3280                // _1 = 0
3281                make_stmt(
3282                    StatementKind::Assign(
3283                        Place::Local(SlotId(1)),
3284                        Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
3285                    ),
3286                    0,
3287                ),
3288                // ClosureCapture { closure_slot: _2, operands: [copy _1] }
3289                make_stmt(
3290                    StatementKind::ClosureCapture {
3291                        closure_slot: SlotId(2),
3292                        operands: vec![Operand::Copy(Place::Local(SlotId(1)))],
3293                        function_id: None,
3294                    },
3295                    1,
3296                ),
3297                // _2 = <closure placeholder>
3298                make_stmt(
3299                    StatementKind::Assign(
3300                        Place::Local(SlotId(2)),
3301                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3302                    ),
3303                    2,
3304                ),
3305                // Reassign outer (proves slot is mutably captured).
3306                make_stmt(
3307                    StatementKind::Assign(
3308                        Place::Local(SlotId(1)),
3309                        Rvalue::Use(Operand::Constant(MirConstant::Int(5))),
3310                    ),
3311                    3,
3312                ),
3313            ],
3314            3,
3315        )
3316    }
3317
3318    #[test]
3319    fn test_phase_d_non_escaping_closure_promotes_outer_slot_to_local_mutable_ptr() {
3320        let mir = phase_d_basic_mir();
3321        let analysis = empty_analysis();
3322        let binding_semantics = HashMap::new();
3323        let mut closure_captures = HashSet::new();
3324        closure_captures.insert(SlotId(1));
3325        let mut mutable_captures = HashSet::new();
3326        mutable_captures.insert(SlotId(1));
3327
3328        let input = StoragePlannerInput {
3329            mir: &mir,
3330            analysis: &analysis,
3331            binding_semantics: &binding_semantics,
3332            closure_captures: &closure_captures,
3333            mutable_captures: &mutable_captures,
3334            had_fallbacks: false,
3335            callee_summaries: None,
3336        };
3337        let plan = plan_storage(&input);
3338
3339        assert_eq!(
3340            plan.slot_classes.get(&SlotId(1)),
3341            Some(&BindingStorageClass::LocalMutablePtr),
3342            "non-escaping mutable capture → LocalMutablePtr"
3343        );
3344        assert!(
3345            plan.non_escaping_closure_slots.contains(&SlotId(2)),
3346            "closure in _2 is non-escaping"
3347        );
3348    }
3349
3350    #[test]
3351    fn test_phase_d_escaping_closure_leaves_outer_slot_as_unique_heap() {
3352        // The closure escapes via return, so its mutable capture must stay
3353        // on the heap — Phase D's demotion is gated on `non_escaping`.
3354        let mut mir = phase_d_basic_mir();
3355        // Replace the trailing statement with `_0 = copy _2` so the closure
3356        // flows into the return slot.
3357        let last_block_idx = mir.blocks.len() - 1;
3358        let stmts = &mut mir.blocks[last_block_idx].statements;
3359        // Drop the reassignment (index 3) and add a return-flow statement.
3360        stmts.pop();
3361        stmts.push(make_stmt(
3362            StatementKind::Assign(
3363                Place::Local(SlotId(0)),
3364                Rvalue::Use(Operand::Copy(Place::Local(SlotId(2)))),
3365            ),
3366            3,
3367        ));
3368
3369        let analysis = empty_analysis();
3370        let binding_semantics = HashMap::new();
3371        let mut closure_captures = HashSet::new();
3372        closure_captures.insert(SlotId(1));
3373        let mut mutable_captures = HashSet::new();
3374        mutable_captures.insert(SlotId(1));
3375
3376        let input = StoragePlannerInput {
3377            mir: &mir,
3378            analysis: &analysis,
3379            binding_semantics: &binding_semantics,
3380            closure_captures: &closure_captures,
3381            mutable_captures: &mutable_captures,
3382            had_fallbacks: false,
3383            callee_summaries: None,
3384        };
3385        let plan = plan_storage(&input);
3386
3387        assert!(
3388            !plan.non_escaping_closure_slots.contains(&SlotId(2)),
3389            "closure is escaping via return"
3390        );
3391        assert_eq!(
3392            plan.slot_classes.get(&SlotId(1)),
3393            Some(&BindingStorageClass::UniqueHeap),
3394            "escaping closure → outer slot stays UniqueHeap"
3395        );
3396    }
3397
3398    #[test]
3399    fn test_phase_d_multiple_disjoint_captures_all_local_mutable_ptr() {
3400        // fn f() { let mut a = 0; let mut b = 0; let c = |x| { a = a + 1; b = b + 2 }; c(); }
3401        // Two disjoint mutably-captured slots by the same non-escaping closure.
3402        // Both should be `LocalMutablePtr`.
3403        let mir = single_block_mir(
3404            "phase_d_disjoint",
3405            vec![
3406                make_stmt(
3407                    StatementKind::Assign(
3408                        Place::Local(SlotId(1)),
3409                        Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
3410                    ),
3411                    0,
3412                ),
3413                make_stmt(
3414                    StatementKind::Assign(
3415                        Place::Local(SlotId(2)),
3416                        Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
3417                    ),
3418                    1,
3419                ),
3420                make_stmt(
3421                    StatementKind::ClosureCapture {
3422                        closure_slot: SlotId(3),
3423                        operands: vec![
3424                            Operand::Copy(Place::Local(SlotId(1))),
3425                            Operand::Copy(Place::Local(SlotId(2))),
3426                        ],
3427                        function_id: None,
3428                    },
3429                    2,
3430                ),
3431                make_stmt(
3432                    StatementKind::Assign(
3433                        Place::Local(SlotId(3)),
3434                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3435                    ),
3436                    3,
3437                ),
3438                // Reassignments to force `mutable_captures` membership.
3439                make_stmt(
3440                    StatementKind::Assign(
3441                        Place::Local(SlotId(1)),
3442                        Rvalue::Use(Operand::Constant(MirConstant::Int(1))),
3443                    ),
3444                    4,
3445                ),
3446                make_stmt(
3447                    StatementKind::Assign(
3448                        Place::Local(SlotId(2)),
3449                        Rvalue::Use(Operand::Constant(MirConstant::Int(2))),
3450                    ),
3451                    5,
3452                ),
3453            ],
3454            4,
3455        );
3456
3457        let analysis = empty_analysis();
3458        let binding_semantics = HashMap::new();
3459        let mut closure_captures = HashSet::new();
3460        closure_captures.insert(SlotId(1));
3461        closure_captures.insert(SlotId(2));
3462        let mut mutable_captures = HashSet::new();
3463        mutable_captures.insert(SlotId(1));
3464        mutable_captures.insert(SlotId(2));
3465
3466        let input = StoragePlannerInput {
3467            mir: &mir,
3468            analysis: &analysis,
3469            binding_semantics: &binding_semantics,
3470            closure_captures: &closure_captures,
3471            mutable_captures: &mutable_captures,
3472            had_fallbacks: false,
3473            callee_summaries: None,
3474        };
3475        let plan = plan_storage(&input);
3476        assert_eq!(
3477            plan.slot_classes.get(&SlotId(1)),
3478            Some(&BindingStorageClass::LocalMutablePtr)
3479        );
3480        assert_eq!(
3481            plan.slot_classes.get(&SlotId(2)),
3482            Some(&BindingStorageClass::LocalMutablePtr)
3483        );
3484    }
3485
3486    // ─────────────────────────────────────────────────────────────────────
3487    // Closure Spec Phase G — snapshot forces all closures to heap
3488    // (docs/v2-closure-specialization.md §5.6)
3489    // ─────────────────────────────────────────────────────────────────────
3490
3491    /// A function that calls `snapshot()` must not stack-allocate any of
3492    /// its closures. The storage planner short-circuits the non-escaping
3493    /// set because `snapshot()` serializes `SerializableCallFrame` from the
3494    /// interpreter's locals array and cannot see Cranelift stack slots.
3495    #[test]
3496    fn test_phase_g_snapshot_call_forces_all_closures_escaping() {
3497        // Bytecode MIR shape (no escape vector fires for the closure slot
3498        // itself):
3499        //   _1 = ClosureCapture(|| 1)
3500        //   _2 = snapshot()        ← terminator: Call(snapshot)
3501        //   _3 = f(5)              ← second block, irrelevant
3502        let bb0 = BasicBlock {
3503            id: BasicBlockId(0),
3504            statements: vec![
3505                make_stmt(
3506                    StatementKind::ClosureCapture {
3507                        closure_slot: SlotId(1),
3508                        operands: vec![],
3509                        function_id: None,
3510                    },
3511                    0,
3512                ),
3513                make_stmt(
3514                    StatementKind::Assign(
3515                        Place::Local(SlotId(1)),
3516                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3517                    ),
3518                    1,
3519                ),
3520            ],
3521            terminator: Terminator {
3522                kind: TerminatorKind::Call {
3523                    func: Operand::Constant(MirConstant::Function(
3524                        "snapshot".to_string(),
3525                    )),
3526                    args: vec![],
3527                    destination: Place::Local(SlotId(2)),
3528                    next: BasicBlockId(1),
3529                },
3530                span: span(),
3531            },
3532        };
3533        let bb1 = BasicBlock {
3534            id: BasicBlockId(1),
3535            statements: vec![],
3536            terminator: make_terminator(TerminatorKind::Return),
3537        };
3538        let mir = make_mir("phase_g_snapshot_escape", vec![bb0, bb1], 3);
3539
3540        let plan = run_planner(&mir);
3541        assert!(
3542            plan.non_escaping_closure_slots.is_empty(),
3543            "snapshot() in function body must force every closure to heap; got {:?}",
3544            plan.non_escaping_closure_slots
3545        );
3546    }
3547
3548    /// Control: the same-shape function WITHOUT `snapshot()` allows the
3549    /// closure to remain non-escaping (no escape vector fires). This
3550    /// proves the previous test's failure mode is specifically the
3551    /// snapshot short-circuit, not a generic escape artifact.
3552    #[test]
3553    fn test_phase_g_no_snapshot_keeps_closure_non_escaping() {
3554        let mir = single_block_mir(
3555            "phase_g_no_snapshot",
3556            vec![
3557                make_stmt(
3558                    StatementKind::ClosureCapture {
3559                        closure_slot: SlotId(1),
3560                        operands: vec![],
3561                        function_id: None,
3562                    },
3563                    0,
3564                ),
3565                make_stmt(
3566                    StatementKind::Assign(
3567                        Place::Local(SlotId(1)),
3568                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3569                    ),
3570                    1,
3571                ),
3572            ],
3573            2,
3574        );
3575
3576        let plan = run_planner(&mir);
3577        assert!(
3578            plan.non_escaping_closure_slots.contains(&SlotId(1)),
3579            "closure with no escape vector (and no snapshot) must be non-escaping; got {:?}",
3580            plan.non_escaping_closure_slots
3581        );
3582    }
3583
3584    /// Snapshot call with a closure also present: both the closure slot
3585    /// AND any local alias of it must end up heap-allocated. Exercises
3586    /// the "snapshot forces deopt / re-materialize" policy from §5.6
3587    /// without needing to actually run the JIT deopt path.
3588    #[test]
3589    fn test_phase_g_snapshot_forces_aliased_closure_to_heap() {
3590        // _1 = ClosureCapture  ; _2 = _1 (alias)  ; snapshot()
3591        let bb0 = BasicBlock {
3592            id: BasicBlockId(0),
3593            statements: vec![
3594                make_stmt(
3595                    StatementKind::ClosureCapture {
3596                        closure_slot: SlotId(1),
3597                        operands: vec![],
3598                        function_id: None,
3599                    },
3600                    0,
3601                ),
3602                make_stmt(
3603                    StatementKind::Assign(
3604                        Place::Local(SlotId(1)),
3605                        Rvalue::Use(Operand::Constant(MirConstant::ClosurePlaceholder)),
3606                    ),
3607                    1,
3608                ),
3609                make_stmt(
3610                    StatementKind::Assign(
3611                        Place::Local(SlotId(2)),
3612                        Rvalue::Use(Operand::Copy(Place::Local(SlotId(1)))),
3613                    ),
3614                    2,
3615                ),
3616            ],
3617            terminator: Terminator {
3618                kind: TerminatorKind::Call {
3619                    func: Operand::Constant(MirConstant::Function(
3620                        "snapshot".to_string(),
3621                    )),
3622                    args: vec![],
3623                    destination: Place::Local(SlotId(3)),
3624                    next: BasicBlockId(1),
3625                },
3626                span: span(),
3627            },
3628        };
3629        let bb1 = BasicBlock {
3630            id: BasicBlockId(1),
3631            statements: vec![],
3632            terminator: make_terminator(TerminatorKind::Return),
3633        };
3634        let mir = make_mir("phase_g_snapshot_aliased", vec![bb0, bb1], 4);
3635
3636        let plan = run_planner(&mir);
3637        assert!(
3638            plan.non_escaping_closure_slots.is_empty(),
3639            "snapshot() with aliased closure must still force every closure slot to heap; \
3640             got {:?}",
3641            plan.non_escaping_closure_slots
3642        );
3643    }
3644
3645    // ─── Phase V0.a: `var` → `SharedCow` default ─────────────────────────
3646    //
3647    // These tests exercise `decide_slot_storage` directly with an explicit
3648    // `var_sharedcow_enabled` flag so they don't depend on — and aren't
3649    // affected by — the process-wide `SHAPE_V2_VAR_SHAREDCOW` env-var cache
3650    // that `plan_storage` consults.
3651
3652    fn run_decide(
3653        slot: SlotId,
3654        mir: &MirFunction,
3655        binding_semantics: &HashMap<u16, BindingSemantics>,
3656        flag_on: bool,
3657    ) -> BindingStorageClass {
3658        let analysis = empty_analysis();
3659        let closure_captures = HashSet::new();
3660        let mutable_captures = HashSet::new();
3661        let input = StoragePlannerInput {
3662            mir,
3663            analysis: &analysis,
3664            binding_semantics,
3665            closure_captures: &closure_captures,
3666            mutable_captures: &mutable_captures,
3667            had_fallbacks: false,
3668            callee_summaries: None,
3669        };
3670        decide_slot_storage(slot, &input, flag_on).0
3671    }
3672
3673    #[test]
3674    fn test_v0a_var_gets_sharedcow_when_flag_on() {
3675        // `var x = 0; x = x + 1;` — no closure capture, no cross-scope
3676        // reference. With the V0.a flag on, Rule 1b fires unconditionally
3677        // because the binding is `Flexible`; with the flag off, the binding
3678        // is neither aliased nor (by the planner's single-rvalue heuristic)
3679        // alias-chained, so it falls through to `Direct`.
3680        let mir = make_mir(
3681            "v0a_var_flag_on",
3682            vec![BasicBlock {
3683                id: BasicBlockId(0),
3684                statements: vec![
3685                    make_stmt(
3686                        StatementKind::Assign(
3687                            Place::Local(SlotId(0)),
3688                            Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
3689                        ),
3690                        0,
3691                    ),
3692                    make_stmt(
3693                        StatementKind::Assign(
3694                            Place::Local(SlotId(0)),
3695                            Rvalue::BinaryOp(
3696                                BinOp::Add,
3697                                Operand::Copy(Place::Local(SlotId(0))),
3698                                Operand::Constant(MirConstant::Int(1)),
3699                            ),
3700                        ),
3701                        1,
3702                    ),
3703                ],
3704                terminator: make_terminator(TerminatorKind::Return),
3705            }],
3706            1,
3707        );
3708
3709        let mut binding_semantics = HashMap::new();
3710        binding_semantics.insert(
3711            0u16,
3712            BindingSemantics::deferred(BindingOwnershipClass::Flexible),
3713        );
3714
3715        // Flag on: Rule 1b promotes `var` → SharedCow regardless of aliasing.
3716        assert_eq!(
3717            run_decide(SlotId(0), &mir, &binding_semantics, true),
3718            BindingStorageClass::SharedCow,
3719            "Phase V0.a: `var` binding with flag on must be SharedCow"
3720        );
3721
3722        // Flag off: Rule 1b is suppressed. The binding is not aliased
3723        // (only one rvalue read), so Rule 3 does not fire. The planner
3724        // falls through to `Direct`.
3725        assert_eq!(
3726            run_decide(SlotId(0), &mir, &binding_semantics, false),
3727            BindingStorageClass::Direct,
3728            "Phase V0.a: `var` binding with flag off falls back to Direct when not aliased+mutated"
3729        );
3730    }
3731
3732    #[test]
3733    fn test_v0a_let_stays_direct_regardless_of_flag() {
3734        // `let x = 0;` — `OwnedImmutable`. Never promoted to SharedCow
3735        // under either flag state.
3736        let mir = make_mir(
3737            "v0a_let_direct",
3738            vec![BasicBlock {
3739                id: BasicBlockId(0),
3740                statements: vec![make_stmt(
3741                    StatementKind::Assign(
3742                        Place::Local(SlotId(0)),
3743                        Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
3744                    ),
3745                    0,
3746                )],
3747                terminator: make_terminator(TerminatorKind::Return),
3748            }],
3749            1,
3750        );
3751
3752        let mut binding_semantics = HashMap::new();
3753        binding_semantics.insert(
3754            0u16,
3755            BindingSemantics::deferred(BindingOwnershipClass::OwnedImmutable),
3756        );
3757
3758        for flag_on in [true, false] {
3759            assert_eq!(
3760                run_decide(SlotId(0), &mir, &binding_semantics, flag_on),
3761                BindingStorageClass::Direct,
3762                "Phase V0.a: `let` binding stays Direct (flag_on = {flag_on})"
3763            );
3764        }
3765    }
3766
3767    #[test]
3768    fn test_v0a_let_mut_not_sharedcow_under_flag() {
3769        // `let mut x = 0; x = x + 1;` — `OwnedMutable`, no closure capture,
3770        // no cross-scope reference. Rule 1b must NOT fire (only Flexible
3771        // bindings get the V0.a promotion). Rule 3 also does not fire
3772        // (OwnedMutable is not Flexible). Slot stays `Direct` under both
3773        // flag states.
3774        let mir = make_mir(
3775            "v0a_let_mut_no_cow",
3776            vec![BasicBlock {
3777                id: BasicBlockId(0),
3778                statements: vec![
3779                    make_stmt(
3780                        StatementKind::Assign(
3781                            Place::Local(SlotId(0)),
3782                            Rvalue::Use(Operand::Constant(MirConstant::Int(0))),
3783                        ),
3784                        0,
3785                    ),
3786                    make_stmt(
3787                        StatementKind::Assign(
3788                            Place::Local(SlotId(0)),
3789                            Rvalue::BinaryOp(
3790                                BinOp::Add,
3791                                Operand::Copy(Place::Local(SlotId(0))),
3792                                Operand::Constant(MirConstant::Int(1)),
3793                            ),
3794                        ),
3795                        1,
3796                    ),
3797                ],
3798                terminator: make_terminator(TerminatorKind::Return),
3799            }],
3800            1,
3801        );
3802
3803        let mut binding_semantics = HashMap::new();
3804        binding_semantics.insert(
3805            0u16,
3806            BindingSemantics::deferred(BindingOwnershipClass::OwnedMutable),
3807        );
3808
3809        for flag_on in [true, false] {
3810            let class = run_decide(SlotId(0), &mir, &binding_semantics, flag_on);
3811            assert!(
3812                matches!(
3813                    class,
3814                    BindingStorageClass::Direct | BindingStorageClass::UniqueHeap
3815                ),
3816                "Phase V0.a: `let mut` must not become SharedCow under V0.a \
3817                 flag (flag_on = {flag_on}); got {class:?}"
3818            );
3819            assert_ne!(
3820                class,
3821                BindingStorageClass::SharedCow,
3822                "Phase V0.a: `let mut` must NEVER become SharedCow (flag_on = {flag_on})"
3823            );
3824        }
3825    }
3826}