Skip to main content

shape_vm/mir/
types.rs

1//! Core MIR types: Place, Statement, Terminator, BasicBlock.
2//!
3//! These represent the mid-level IR that the borrow solver operates on.
4//! Places track what can be borrowed (locals, fields, indices).
5//! Statements and terminators form basic blocks in a control flow graph.
6
7use shape_ast::ast::{Span, TypeAnnotation};
8use std::fmt;
9
10// ── Identifiers ──────────────────────────────────────────────────────
11
12/// Index of a local variable slot.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct SlotId(pub u16);
15
16/// Index of a struct/object field.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct FieldIdx(pub u16);
19
20/// Index of a basic block within a MIR function.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct BasicBlockId(pub u32);
23
24/// A program point (statement index within the function's linearized MIR).
25/// Used as the "point" dimension in Datafrog relations.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct Point(pub u32);
28
29/// Unique identifier for a loan (borrow).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
31pub struct LoanId(pub u32);
32
33/// A normalized step in a place projection chain.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum ProjectionStep {
36    Field(FieldIdx),
37    /// Index projections are intentionally summarized without their concrete
38    /// operand. The borrow solver only needs to know that an index boundary
39    /// exists for provenance and diagnostics.
40    Index,
41}
42
43impl fmt::Display for SlotId {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "_{}", self.0)
46    }
47}
48
49impl fmt::Display for BasicBlockId {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "bb{}", self.0)
52    }
53}
54
55impl fmt::Display for Point {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(f, "p{}", self.0)
58    }
59}
60
61impl fmt::Display for LoanId {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "L{}", self.0)
64    }
65}
66
67// ── Places ───────────────────────────────────────────────────────────
68
69/// A place is something that can be borrowed or assigned to.
70/// Tracks granular access paths for disjoint borrow analysis.
71#[derive(Debug, Clone, PartialEq, Eq, Hash)]
72pub enum Place {
73    /// A local variable: `x`
74    Local(SlotId),
75    /// A field of a place: `x.field_name`
76    Field(Box<Place>, FieldIdx),
77    /// An index into a place: `x[i]` — index analysis is conservative in v1.
78    /// The index operand is boxed to break the recursive type cycle (Place → Operand → Place).
79    Index(Box<Place>, Box<Operand>),
80    /// Dereferencing a reference: `*r`
81    Deref(Box<Place>),
82}
83
84impl Place {
85    /// Get the root local of this place (e.g., `x.a.b` → `x`).
86    pub fn root_local(&self) -> SlotId {
87        match self {
88            Place::Local(slot) => *slot,
89            Place::Field(base, _) | Place::Index(base, _) | Place::Deref(base) => base.root_local(),
90        }
91    }
92
93    /// Check if this place is a prefix of another (for conflict detection).
94    /// `x` is a prefix of `x.a`, `x` is a prefix of `x[i]`, etc.
95    pub fn is_prefix_of(&self, other: &Place) -> bool {
96        if self == other {
97            return true;
98        }
99        match other {
100            Place::Local(_) => false,
101            Place::Field(base, _) | Place::Index(base, _) | Place::Deref(base) => {
102                self.is_prefix_of(base)
103            }
104        }
105    }
106
107    /// Check whether two places conflict (one borrows/writes something the other uses).
108    /// Two places conflict if one is a prefix of the other, or they're the same.
109    /// In v1, disjoint field borrows are tracked (x.a and x.b don't conflict),
110    /// but index borrows are conservative (x[i] and x[j] always conflict).
111    pub fn conflicts_with(&self, other: &Place) -> bool {
112        // Same root?
113        if self.root_local() != other.root_local() {
114            return false;
115        }
116        // Walk both paths to check overlap
117        self.is_prefix_of(other) || other.is_prefix_of(self) || self.overlaps(other)
118    }
119
120    fn overlaps(&self, other: &Place) -> bool {
121        match (self, other) {
122            (Place::Local(a), Place::Local(b)) => a == b,
123            // Disjoint fields: x.a and x.b do NOT conflict
124            (Place::Field(base_a, field_a), Place::Field(base_b, field_b)) => {
125                if base_a == base_b {
126                    field_a == field_b
127                } else {
128                    base_a.overlaps(base_b)
129                }
130            }
131            // Conservative: x[i] and x[j] always conflict
132            (Place::Index(base_a, _), Place::Index(base_b, _)) => base_a.overlaps(base_b),
133            _ => self.is_prefix_of(other) || other.is_prefix_of(self),
134        }
135    }
136
137    /// Return a normalized projection summary from the root local to this place.
138    pub fn projection_steps(&self) -> Vec<ProjectionStep> {
139        let mut steps = Vec::new();
140        self.collect_projection_steps(&mut steps);
141        steps
142    }
143
144    fn collect_projection_steps(&self, steps: &mut Vec<ProjectionStep>) {
145        match self {
146            Place::Local(_) => {}
147            Place::Field(base, field) => {
148                base.collect_projection_steps(steps);
149                steps.push(ProjectionStep::Field(*field));
150            }
151            Place::Index(base, _) => {
152                base.collect_projection_steps(steps);
153                steps.push(ProjectionStep::Index);
154            }
155            Place::Deref(base) => base.collect_projection_steps(steps),
156        }
157    }
158}
159
160impl fmt::Display for Place {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        match self {
163            Place::Local(slot) => write!(f, "{}", slot),
164            Place::Field(base, field) => write!(f, "{}.{}", base, field.0),
165            Place::Index(base, idx) => write!(f, "{}[{}]", base, idx),
166            Place::Deref(base) => write!(f, "*{}", base),
167        }
168    }
169}
170
171// ── Operands ─────────────────────────────────────────────────────────
172
173/// An operand in an Rvalue or terminator.
174#[derive(Debug, Clone, PartialEq, Eq, Hash)]
175pub enum Operand {
176    /// Copy the value from a place (for Copy types).
177    Copy(Place),
178    /// Move the value from a place (invalidates the source).
179    Move(Place),
180    /// Explicit source-level move (`move x`) that must not be rewritten into a clone.
181    MoveExplicit(Place),
182    /// A constant value.
183    Constant(MirConstant),
184}
185
186impl fmt::Display for Operand {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            Operand::Copy(p) => write!(f, "copy {}", p),
190            Operand::Move(p) => write!(f, "move {}", p),
191            Operand::MoveExplicit(p) => write!(f, "move! {}", p),
192            Operand::Constant(c) => write!(f, "{}", c),
193        }
194    }
195}
196
197/// A constant value in MIR.
198#[derive(Debug, Clone, PartialEq, Eq, Hash)]
199pub enum MirConstant {
200    Int(i64),
201    Bool(bool),
202    None,
203    /// String interned index (legacy — prefer Str for new code)
204    StringId(u32),
205    /// String literal value (carried through MIR for direct JIT materialization)
206    Str(String),
207    /// Float (stored as bits for Eq/Hash)
208    Float(u64),
209    /// Decimal literal — carried through MIR as the source-form lexeme.
210    ///
211    /// WS-8 (2026-05-22): pre-WS-8 `Literal::Decimal(_)` MIR lowering at
212    /// `mir/lowering/expr.rs:1937` collapsed to `MirConstant::Float(0)` ("decimal
213    /// not yet modeled"), silently losing the value. The JIT then printed
214    /// `0.0` for `print(1.5D)` while the VM printed `1.5D` — a v0.3-gating
215    /// silent wrong-answer divergence (WS-8 audit §1.D). The MIR producer now
216    /// emits this variant verbatim; the JIT consumer SURFACEs (`compile_constant`
217    /// returns Err), triggering the W12 fall-through to the bytecode interpreter
218    /// (which materializes the decimal via the VM's `NewDecimalV2` opcode and
219    /// prints correctly). VM == JIT, both run the interpreter path. The variant
220    /// stores the decimal's lexeme so JIT codegen can light up later without
221    /// re-parsing the AST.
222    Decimal(String),
223    /// Character literal (scalar codepoint).
224    ///
225    /// Phase 3 cluster-2 Round 4 cw-D-fam12 follow-up (instance 57, 2026-05-16).
226    /// ADR-006 §2.7.5 amendment Round 19 S1.5 W12-nativekind-scalar-additions
227    /// (2026-05-14): `Char` is a 4-byte scalar `NativeKind` variant (codepoint
228    /// in low 32 bits of `ValueSlot`, no Arc wrapping). Producing-site
229    /// stamp-at-compile-time discipline requires the MIR layer to preserve the
230    /// Char kind through to the JIT's `operand_slot_kind` / `infer_constant_kind`
231    /// classifiers — otherwise `Literal::Char('A')` is lost as `MirConstant::Int(65)`
232    /// at MIR lowering, the JIT stamps `NativeKind::Int64`, and the `print`
233    /// dispatch matches `print_i64(65)` instead of `print_char(65)` → JIT prints
234    /// "65" while VM prints "A" (cw-D-fam12 Char production-fixture divergence).
235    Char(char),
236    /// Function reference by name
237    Function(String),
238    /// Method name for dispatch
239    Method(String),
240    /// Placeholder for a closure function reference.
241    /// Patched to `Function(name)` after bytecode compilation resolves the closure's function_id.
242    ClosurePlaceholder,
243}
244
245impl fmt::Display for MirConstant {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            MirConstant::Int(v) => write!(f, "{}", v),
249            MirConstant::Bool(v) => write!(f, "{}", v),
250            MirConstant::None => write!(f, "none"),
251            MirConstant::StringId(id) => write!(f, "str#{}", id),
252            MirConstant::Str(s) => write!(f, "\"{}\"", s),
253            MirConstant::Float(bits) => write!(f, "{}", f64::from_bits(*bits)),
254            MirConstant::Decimal(s) => write!(f, "{}D", s),
255            MirConstant::Char(c) => write!(f, "'{}'", c.escape_default()),
256            MirConstant::Function(name) => write!(f, "fn:{}", name),
257            MirConstant::Method(name) => write!(f, "method:{}", name),
258            MirConstant::ClosurePlaceholder => write!(f, "closure_placeholder"),
259        }
260    }
261}
262
263// ── Rvalues ──────────────────────────────────────────────────────────
264
265/// The kind of borrow.
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
267pub enum BorrowKind {
268    /// Shared (immutable) borrow: `&x`
269    Shared,
270    /// Exclusive (mutable) borrow: `&mut x`
271    Exclusive,
272}
273
274impl fmt::Display for BorrowKind {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        match self {
277            BorrowKind::Shared => write!(f, "&"),
278            BorrowKind::Exclusive => write!(f, "&mut"),
279        }
280    }
281}
282
283/// Result/Option variant tag — classification is producer-side per
284/// ADR-006 §2.7.5 stamp-at-compile-time. Carried by `Rvalue::EnumTest`
285/// and `Rvalue::EnumPayload`; the JIT consumer dispatches on this enum
286/// directly, never decodes from bits (§2.7.7 #4 / #7 forbidden).
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
288pub enum VariantTag {
289    Ok,
290    Err,
291    Some_,
292    None_,
293}
294
295impl fmt::Display for VariantTag {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        match self {
298            VariantTag::Ok => write!(f, "Ok"),
299            VariantTag::Err => write!(f, "Err"),
300            VariantTag::Some_ => write!(f, "Some"),
301            VariantTag::None_ => write!(f, "None"),
302        }
303    }
304}
305
306impl VariantTag {
307    /// Map a constructor name to a VariantTag. Returns `None` for non-
308    /// builtin (`Ok`/`Err`/`Some`/`None`) names so the producer site can
309    /// fall back to the generic `Aggregate` / `EnumStore` path for user-
310    /// defined enum variants.
311    #[inline]
312    pub fn from_name(name: &str) -> Option<Self> {
313        match name {
314            "Ok" => Some(VariantTag::Ok),
315            "Err" => Some(VariantTag::Err),
316            "Some" => Some(VariantTag::Some_),
317            "None" => Some(VariantTag::None_),
318            _ => None,
319        }
320    }
321}
322
323/// Right-hand side of an assignment.
324#[derive(Debug, Clone, PartialEq)]
325pub enum Rvalue {
326    /// Use an operand directly.
327    Use(Operand),
328    /// Create a borrow: `&place` or `&mut place`
329    Borrow(BorrowKind, Place),
330    /// Binary operation.
331    BinaryOp(BinOp, Operand, Operand),
332    /// Unary operation.
333    UnaryOp(UnOp, Operand),
334    /// Function call result (arguments passed via terminator).
335    /// This is a placeholder — actual calls use Call terminators.
336    Aggregate(Vec<Operand>),
337    /// Clone of a value (explicit or auto-inferred).
338    Clone(Operand),
339    /// Test whether a Result/Option scrutinee matches a specific variant
340    /// (per ADR-006 §2.7.17 / Q18 — kinded `Arc<ResultData>` /
341    /// `Arc<OptionData>` carrier). Result: native Bool (`I8`).
342    ///
343    /// Emitted by `lower_match_pattern_condition_operand` when the scrutinee
344    /// is a `Pattern::Constructor` with a recognised `VariantTag::Ok` /
345    /// `Err` / `Some_` / `None_`. JIT consumer dispatches to the
346    /// `jit_arc_result_is_ok` / `_is_err` / `jit_arc_option_is_some` /
347    /// `_is_none` FFI which reads `is_ok` / `is_some` from the
348    /// `*const ResultData` / `*const OptionData` directly per §2.7.17
349    /// — no NaN-box tag decode, no `is_heap_kind` probe (§2.7.7 #4 / #7
350    /// forbidden).
351    ///
352    /// Producer-side classification per W12-jit-result-option-trinity
353    /// audit (`docs/cluster-audits/w12-jit-match-enum-inline-audit.md` §6.1).
354    EnumTest {
355        operand: Operand,
356        variant: VariantTag,
357    },
358    /// Extract the inner payload bits from a Result/Option scrutinee.
359    /// Caller must have already proven the variant via `EnumTest`
360    /// (control flow guarantees the matching arm is entered only when
361    /// the variant matches; the `variant` tag here is the producer-side
362    /// classification for kind sourcing, NOT a runtime check).
363    ///
364    /// Result: raw `u64` payload bits — the payload's kind flows out of
365    /// band via 6A's Call-return-kind track + the EnumStore producer's
366    /// kind stamp.
367    ///
368    /// JIT consumer dispatches to `jit_arc_result_payload` /
369    /// `jit_arc_option_payload` which read the inner `KindedSlot.raw()`
370    /// from the `*const ResultData` / `*const OptionData` and bump the
371    /// inner refcount per the receiver-recovery soundness rule —
372    /// the returned bits are an owned slot.
373    ///
374    /// `VariantTag::None_` is rejected at consumer time (no payload to
375    /// extract — None's payload field is a zero-bits Bool placeholder
376    /// per ADR-006 §2.7.17 `OptionData::none()`).
377    ///
378    /// Producer-side classification per W12-jit-result-option-trinity
379    /// audit §6.2.
380    EnumPayload {
381        operand: Operand,
382        variant: VariantTag,
383    },
384    /// Test whether the scrutinee's runtime kind matches a `Pattern::Typed`
385    /// type annotation (e.g. `match x { n: int => ..., s: string => ... }`).
386    /// Result: native Bool (`I8`).
387    ///
388    /// W15.2-LANG-5 (Phase 4b, 2026-05-18). Pre-fix the MIR lowering of
389    /// `Pattern::Typed` returned `None` for the condition operand — same
390    /// shape as `Pattern::Wildcard`/`Pattern::Identifier` — so every typed
391    /// match arm was reached via `TerminatorKind::Goto` with NO type
392    /// discrimination. The first arm always won and the union-scrutinee
393    /// silently took the wrong branch under JIT.
394    ///
395    /// Producer-side classification per ADR-006 §2.7.5 stamp-at-compile-time:
396    /// the type annotation is carried verbatim from `ast::Pattern::Typed`
397    /// so consumers do not re-derive it from the operand bits.
398    ///
399    /// Consumer status:
400    /// - JIT MIR preflight (`shape-jit::mir_compiler::preflight`) REJECTS
401    ///   on this Rvalue → W12 fall-through routes the program to the
402    ///   bytecode interpreter, which compiles the same scrutinee via the
403    ///   `OpCode::TypeCheck` path in `compiler/patterns/checking.rs`.
404    ///   Native JIT codegen lands as a follow-up (`jit_type_check` FFI +
405    ///   per-kind dispatch on the §2.7.7 stack parallel-kind track).
406    /// - VM never consumes MIR; `compile_match_expr` in
407    ///   `compiler/expressions/advanced.rs` calls `compile_pattern_check`
408    ///   directly on the AST and emits `OpCode::TypeCheck` itself.
409    ///
410    /// The annotation IS the producer-side classification — neither a
411    /// Bool-default fabrication nor any of the deleted dispatch families
412    /// enumerated under CLAUDE.md Forbidden Patterns.
413    TypePatternTest {
414        operand: Operand,
415        type_annotation: TypeAnnotation,
416    },
417    /// Test whether the scrutinee's enum discriminant matches a specific
418    /// user-defined `Pattern::Constructor` variant (e.g. `match c {
419    /// Color::Red => ..., Color::Green => ... }`). Result: native Bool
420    /// (`I8`).
421    ///
422    /// W15.2-LANG-1 (Phase 4b, 2026-05-18). Pre-fix the MIR lowering of
423    /// `Pattern::Constructor` for non-trinity (non-`Ok`/`Err`/`Some`/`None`)
424    /// variants returned `Some(Operand::Copy(Place::Local(scrutinee_slot)))`
425    /// — the raw `Arc<TypedObjectStorage>` pointer bits — as the
426    /// `SwitchBool` condition. The JIT consumer's generic I64-truthy
427    /// check at `terminators.rs::SwitchBool` then evaluated the non-zero
428    /// pointer as `true` for the first arm OR fell to false-branch when
429    /// the multi-arm dispatch chain looped past the first arm, producing
430    /// silent-empty-output for the user's `match Color::Red { Color::Red
431    /// => print("red"), ... }` case (book snippet `enums.mdx:113`).
432    ///
433    /// Producer-side classification per ADR-006 §2.7.5 stamp-at-compile-
434    /// time: the enum name + variant name are carried verbatim from
435    /// `ast::Pattern::Constructor` so the consumer dispatches on a known
436    /// (`enum_name`, `variant_name`) pair, NEVER on raw scrutinee bits.
437    ///
438    /// Consumer status:
439    /// - JIT MIR preflight (`shape-jit::mir_compiler::preflight`) REJECTS
440    ///   on this Rvalue → W12 fall-through routes the program to the
441    ///   bytecode interpreter, which compiles the same scrutinee via the
442    ///   `compile_typed_enum_pattern_check` path in
443    ///   `compiler/patterns/checking.rs` (emits `GetFieldTyped(__variant,
444    ///   I64)` + `PushConst(expected_variant_id)` + `EqInt`).
445    /// - VM never consumes MIR; `compile_match_expr` in
446    ///   `compiler/expressions/advanced.rs` calls `compile_pattern_check`
447    ///   directly on the AST and emits the typed-object discriminant
448    ///   check itself.
449    ///
450    /// The (enum_name, variant_name) pair IS the producer-side
451    /// classification — neither a Bool-default fabrication nor any of
452    /// the deleted dispatch families enumerated under CLAUDE.md
453    /// Forbidden Patterns. Mirrors the LANG-5 `TypePatternTest`
454    /// precedent (W15.2-LANG-5 close 2026-05-18) for `Pattern::Typed`.
455    EnumDiscriminantTest {
456        operand: Operand,
457        enum_name: Option<String>,
458        variant_name: String,
459    },
460}
461
462/// Binary operations in MIR.
463///
464/// W11-fup-A (Phase 3d, 2026-05-18) extends this enum with `Pow` + the five
465/// bitwise variants (`BitAnd`/`BitOr`/`BitXor`/`BitShl`/`BitShr`) per the
466/// W11-jit-new-array close §4 Class A residual: the bytecode VM already
467/// emits typed opcodes for these operators (`PowInt`/`PowNumber`,
468/// `BitAndInt`/`BitOrInt`/`BitXorInt`/`BitShlInt`/`BitShrInt` at
469/// `crates/shape-vm/src/bytecode/opcode_defs.rs:317-322 / 1860-1873`); the
470/// MIR layer was the gap forcing `lower_binary_op` to fall through to
471/// `Rvalue::Aggregate(vec![l, r])` and surface-and-stop in JIT (`Route A`
472/// at `crates/shape-jit/src/mir_compiler/rvalues.rs:145`).
473///
474/// Fuzzy ops + `NullCoalesce` / `ErrorContext` / `Pipe` remain unhandled
475/// here per the same close doc's "different semantics" disposition — those
476/// are tracked by their own follow-up sub-clusters.
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478pub enum BinOp {
479    Add,
480    Sub,
481    Mul,
482    Div,
483    Mod,
484    Pow,
485    BitAnd,
486    BitOr,
487    BitXor,
488    BitShl,
489    BitShr,
490    Eq,
491    Ne,
492    Lt,
493    Le,
494    Gt,
495    Ge,
496    And,
497    Or,
498}
499
500/// Unary operations in MIR.
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub enum UnOp {
503    Neg,
504    Not,
505    // W14.2-A1 (Phase 4b, 2026-05-18): `BitNot` (`~x`) lowers to native
506    // Int64 bitwise-NOT, mirroring the bytecode VM's `BitNotInt` typed
507    // opcode (`arithmetic/mod.rs:229`). The MIR enum extension mirrors
508    // the W11-fup-A `BinOp` Pow/BitAnd/BitOr/BitXor/Shl/Shr pattern at
509    // `mir/types.rs:373-407` (close commit `46be6b0d`) — without this
510    // variant, `lower_unary_op(BitNot)` returned `None` and the
511    // expression fell through to the kind-blind `Rvalue::Aggregate(vec![
512    // operand])` arm at `mir/lowering/expr.rs:1722-1727`, which the JIT
513    // consumer surface-and-stops as W11-followup-unop-bitnot per
514    // W11-fup-A close §"Residuals" line 4 ("Class F NEW UnOp::BitNot:
515    // op_bitwise JIT → W11-followup-unop-bitnot").
516    BitNot,
517}
518
519// ── Task Boundary Kind ───────────────────────────────────────────────
520
521/// Distinguishes detached vs structured async task boundaries.
522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523pub enum TaskBoundaryKind {
524    /// Detached async task (not joined in declaring scope).
525    Detached,
526    /// Structured child task (joined before parent scope exits).
527    Structured,
528}
529
530// ── Statements ───────────────────────────────────────────────────────
531
532/// A statement within a basic block (doesn't affect control flow).
533#[derive(Debug, Clone, PartialEq)]
534pub struct MirStatement {
535    pub kind: StatementKind,
536    pub span: Span,
537    /// The program point of this statement (assigned during linearization).
538    pub point: Point,
539}
540
541#[derive(Debug, Clone, PartialEq)]
542pub enum StatementKind {
543    /// Assign a value to a place: `place = rvalue`
544    Assign(Place, Rvalue),
545    /// Drop a place (scope exit, explicit drop).
546    /// Generates invalidation facts for any loans on this place.
547    Drop(Place),
548    /// Cross a task boundary (spawn/join branch capture).
549    /// Operands are the values flowing into the spawned task.
550    /// The kind distinguishes detached vs structured tasks.
551    TaskBoundary(Vec<Operand>, TaskBoundaryKind),
552    /// Capture values into a closure environment.
553    /// Operands are the outer values flowing into the closure.
554    /// `function_id` is patched after bytecode compilation resolves the closure's index.
555    ClosureCapture {
556        closure_slot: SlotId,
557        operands: Vec<Operand>,
558        function_id: Option<u16>,
559    },
560    /// Store values into an array literal.
561    /// Operands are the array elements being stored.
562    ArrayStore {
563        container_slot: SlotId,
564        operands: Vec<Operand>,
565    },
566    /// Store values into an object or struct literal.
567    /// Operands are the fields/spreads being stored.
568    /// `field_names` carries the string key for each operand (from the AST).
569    /// When present, JIT codegen can construct a proper object with named fields.
570    ///
571    /// `schema_id` carries the user-declared (or anonymous-inline) schema id
572    /// from the bytecode-side `OpCode::NewTypedObject` operand
573    /// (`Operand::TypedObjectAlloc { schema_id, field_count }`). MIR lowering
574    /// emits `None`; the bytecode compiler back-patches the resolved
575    /// `SchemaId` via `crate::compiler::mir_schema_threading::
576    /// back_patch_schema_ids` (Phase 3 cluster-0 Round 16 W17-narrow-
577    /// follow-up-A, ADR-006 §2.7.5 stamp-at-compile-time). The JIT MIR
578    /// consumer at `crates/shape-jit/src/mir_compiler/statements.rs::
579    /// StatementKind::ObjectStore` uses this id directly for
580    /// `typed_object_alloc`, preserving the user-declared schema identity
581    /// (e.g. `X` schema = 53 in Smoke 3) instead of the prior
582    /// `register_predeclared_any_schema` `__predecl_*`-named id (54).
583    ///
584    /// `None` when the back-patch could not resolve the schema (the
585    /// downstream JIT consumer surfaces-and-stops per §2.7.5 — no
586    /// `register_predeclared_any_schema` fallback, no Bool-default).
587    ObjectStore {
588        container_slot: SlotId,
589        operands: Vec<Operand>,
590        field_names: Vec<String>,
591        schema_id: Option<u32>,
592    },
593    /// Store values into an enum payload.
594    /// Operands are the tuple/struct payload values being stored.
595    ///
596    /// `variant_name` carries the constructor name (Ok / Err / Some /
597    /// user-defined variant) — known at MIR-lowering time and threaded
598    /// through so the JIT EnumStore consumer can dispatch to the right
599    /// typed-Arc producer (`jit_v2_make_result_ok` / `_err` /
600    /// `jit_v2_make_option_some`). `None` is permitted for paths that
601    /// haven't been migrated to thread the variant; downstream JIT
602    /// consumers surface-and-stop on `None` for non-empty payloads per
603    /// ADR-006 §2.7.5 / §2.7.7 #9 (no Bool-default fallback).
604    EnumStore {
605        container_slot: SlotId,
606        operands: Vec<Operand>,
607        variant_name: Option<String>,
608    },
609    /// No-op (placeholder, padding).
610    Nop,
611}
612
613// ── Terminators ──────────────────────────────────────────────────────
614
615/// A block terminator (controls flow between basic blocks).
616#[derive(Debug, Clone, PartialEq)]
617pub struct Terminator {
618    pub kind: TerminatorKind,
619    pub span: Span,
620}
621
622#[derive(Debug, Clone, PartialEq)]
623pub enum TerminatorKind {
624    /// Unconditional jump.
625    Goto(BasicBlockId),
626    /// Conditional branch.
627    SwitchBool {
628        operand: Operand,
629        true_bb: BasicBlockId,
630        false_bb: BasicBlockId,
631    },
632    /// Function call.
633    Call {
634        func: Operand,
635        args: Vec<Operand>,
636        /// Where to store the return value.
637        destination: Place,
638        /// Block to jump to after the call returns.
639        next: BasicBlockId,
640    },
641    /// Return from function.
642    Return,
643    /// Unreachable (after diverging calls, infinite loops).
644    Unreachable,
645}
646
647// ── Basic Blocks ─────────────────────────────────────────────────────
648
649/// A basic block: a sequence of statements ending in a terminator.
650#[derive(Debug, Clone)]
651pub struct BasicBlock {
652    pub id: BasicBlockId,
653    pub statements: Vec<MirStatement>,
654    pub terminator: Terminator,
655}
656
657// ── MIR Function ─────────────────────────────────────────────────────
658
659/// The MIR representation of a single function.
660#[derive(Debug, Clone)]
661pub struct MirFunction {
662    pub name: String,
663    /// The basic blocks forming the CFG.
664    pub blocks: Vec<BasicBlock>,
665    /// Number of local variable slots.
666    pub num_locals: u16,
667    /// Which locals are function parameters.
668    pub param_slots: Vec<SlotId>,
669    /// Per-parameter reference kind, aligned with `param_slots`.
670    pub param_reference_kinds: Vec<Option<BorrowKind>>,
671    /// Type information for locals (for Copy/Clone inference).
672    pub local_types: Vec<LocalTypeInfo>,
673    /// Source span of the function.
674    pub span: Span,
675    /// Mapping from FieldIdx to field name, for JIT field access resolution.
676    pub field_name_table: std::collections::HashMap<FieldIdx, String>,
677    /// Per-slot user-struct type name for slots produced by
678    /// `Expr::StructLiteral { name, .. }` lowering.
679    ///
680    /// ADR-006 §2.7.5 producing-site classification — Phase 3 cluster-0
681    /// Round 13 T1' gap 1 closure. The bytecode compiler's
682    /// `concrete_type_from_annotation`
683    /// (`crates/shape-vm/src/compiler/v2_map_emission.rs:357`) does
684    /// NOT resolve user-struct names to a per-struct `StructLayoutId`
685    /// (the `_ => None` arm at line 378), and the conduit producer at
686    /// `compiler/helpers.rs:508` stamps `Struct(StructLayoutId(0))`
687    /// for every `ObjectStore` regardless of struct identity. Neither
688    /// path makes user-struct type name observable at conduit-time.
689    ///
690    /// This map is populated at MIR lowering for
691    /// `Expr::StructLiteral { name, .. }` sites (the canonical
692    /// user-struct construction shape — `let t = X {}`,
693    /// `let p = Point { x: 1, y: 2 }`). The conduit producer reads
694    /// the map at Call-terminator destination-stamp time for
695    /// `MirConstant::Method(_)` terminators to look up the trait method
696    /// declared return ConcreteType via the
697    /// `find_default_trait_impl_for_type_method` chain.
698    ///
699    /// `None` (no entry) for slots that aren't user-struct constructions
700    /// — primitives, collections, plain object literals, function returns,
701    /// etc. The downstream classifier surfaces unstamped per §2.7.7 #9 /
702    /// forbidden #9 (no fabricated default).
703    pub local_struct_type_names:
704        std::collections::HashMap<SlotId, String>,
705    /// Per-slot empty-typed-array element ConcreteType for slots produced by
706    /// `let mut <name>: Array<C> = []` lowering (where `C` is a
707    /// `concrete_type_from_annotation`-resolvable element type).
708    ///
709    /// ADR-006 §2.7.5 stamp-at-compile-time — V3-S6e-jit-specialized-vec-
710    /// map-aggregate-classify (Phase 3 cluster-0+1 Wave 3 Stabilize Round 2,
711    /// 2026-05-16; V3-S6 multi-session chain checkpoint-final).
712    ///
713    /// Closes the W11-jit-new-array gap inside monomorphized `Vec.map<U>` /
714    /// `Vec.filter<U>` specialization bodies. V3-S6a's
715    /// `synthesize_empty_array_result_annotation` writes
716    /// `Array<C>` onto the `let mut result = []` var-decl AST node for the
717    /// specialized function; this map captures that annotation at MIR
718    /// lowering so the conduit producer at
719    /// `crates/shape-vm/src/compiler/helpers.rs::infer_top_level_concrete_
720    /// types_from_mir_with_resolvers` can stamp `concrete_types[result_slot]
721    /// = Array(elem)`.
722    ///
723    /// Why MIR-level rather than just bytecode-side: the empty array literal
724    /// at MIR lowering goes through `lower_array_expr` →
725    /// `emit_container_store_if_needed` which short-circuits for
726    /// `ContainerStoreKind::Array` with empty operands (helpers.rs:128-130).
727    /// No `StatementKind::ArrayStore` is emitted, so the
728    /// `helpers.rs:687` ArrayStore walker never fires on empty literals.
729    /// The conduit producer has no other source for the element kind on
730    /// an empty Aggregate slot.
731    ///
732    /// Producer: `mir/lowering/stmt.rs::lower_var_decl` (var-decl with
733    /// annotation `Array<C>` AND value `Expr::Array(items)` with
734    /// `items.is_empty()`).
735    /// Consumer: `compiler/helpers.rs::infer_top_level_concrete_types_from_
736    /// mir_with_resolvers` (new pass before slot-move propagation).
737    ///
738    /// `None` (no entry) for slots that aren't empty-typed-array-literal
739    /// initializations — non-empty array literals flow through the existing
740    /// `ArrayStore` operand-kind inference path; absent annotation means
741    /// no proven element kind and the JIT surfaces-and-stops per §2.7.7 #9
742    /// forbidden Bool-default.
743    pub local_typed_array_element_types:
744        std::collections::HashMap<SlotId, shape_value::v2::ConcreteType>,
745    /// Per-slot declared scalar `ConcreteType` for `let`-bindings whose
746    /// type annotation resolves to a narrow integer width (`i8`/`i16`/
747    /// `i32`/`u8`/`u16`/`u32`).
748    ///
749    /// ADR-006 §2.7.5 stamp-at-compile-time — R5c-2-β-γ (c)
750    /// jit-narrow-wrap. The MIR `Rvalue::Use(Constant(Int(_)))` carrier
751    /// is width-blind: a bare integer literal has no width, so
752    /// `infer_top_level_concrete_types_from_mir` classifies every
753    /// `MirConstant::Int` as `ConcreteType::I64`. Top-level `let a: i32 =
754    /// 100` bindings are module bindings (not bytecode locals — see
755    /// `compiler/compiler_impl_reference_model.rs:1472`), so the
756    /// bytecode-compiler's per-local side-tables never carry the
757    /// declared narrow width either. Without a width source the JIT
758    /// declares the slot as a Cranelift `I64` variable and emits
759    /// `iadd`/`isub`/`imul` at 64-bit width — overflow does not wrap to
760    /// the declared width, diverging from the bytecode VM's
761    /// `AddI32`/`AddTyped` truncating opcodes.
762    ///
763    /// This map is populated at MIR lowering for var-decls whose
764    /// `type_annotation` resolves through `concrete_type_from_annotation`
765    /// to one of the narrow integer scalar `ConcreteType` variants. The
766    /// conduit producer
767    /// (`compiler/helpers.rs::infer_top_level_concrete_types_from_mir_
768    /// with_resolvers`) reads it to stamp `concrete_types[slot]` with the
769    /// proven narrow width, which the JIT then projects to
770    /// `NativeKind::Int32`/`Int8`/etc., declares the slot at the matching
771    /// Cranelift width, and lowers arithmetic so overflow wraps.
772    ///
773    /// `None` (no entry) for slots without a narrow-int annotation —
774    /// plain `int`/`number`/heap types are unaffected.
775    ///
776    /// Producer: `mir/lowering/stmt.rs::lower_var_decl`.
777    /// Consumer: `compiler/helpers.rs::infer_top_level_concrete_types_
778    /// from_mir_with_resolvers`.
779    pub local_declared_scalar_types:
780        std::collections::HashMap<SlotId, shape_value::v2::ConcreteType>,
781}
782
783/// Type information for a local variable, used for Copy/Clone inference.
784#[derive(Debug, Clone, PartialEq, Eq)]
785pub enum LocalTypeInfo {
786    /// Primitive (int, number, bool, none) — implicitly Copy, no borrow tracking.
787    Copy,
788    /// Heap type (String, Array, TypedObject, etc.) — requires borrow/move/clone tracking.
789    NonCopy,
790    /// Unknown type (will be resolved during analysis).
791    Unknown,
792}
793
794impl MirFunction {
795    /// Get the entry block (always block 0).
796    pub fn entry_block(&self) -> BasicBlockId {
797        BasicBlockId(0)
798    }
799
800    /// Iterate over all blocks.
801    pub fn iter_blocks(&self) -> impl Iterator<Item = &BasicBlock> {
802        self.blocks.iter()
803    }
804
805    /// Get a block by ID.
806    pub fn block(&self, id: BasicBlockId) -> &BasicBlock {
807        &self.blocks[id.0 as usize]
808    }
809
810    /// Linearize all statements into a flat list of points.
811    /// Returns (point, block_id, statement_index) triples.
812    pub fn all_points(&self) -> Vec<(Point, BasicBlockId, usize)> {
813        let mut points = Vec::new();
814        for block in &self.blocks {
815            for (i, stmt) in block.statements.iter().enumerate() {
816                points.push((stmt.point, block.id, i));
817            }
818        }
819        points
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826
827    #[test]
828    fn test_place_root_local() {
829        let p = Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(1));
830        assert_eq!(p.root_local(), SlotId(0));
831    }
832
833    #[test]
834    fn test_place_prefix() {
835        let x = Place::Local(SlotId(0));
836        let xa = Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(0));
837        assert!(x.is_prefix_of(&xa));
838        assert!(!xa.is_prefix_of(&x));
839    }
840
841    #[test]
842    fn test_disjoint_fields_no_conflict() {
843        let xa = Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(0));
844        let xb = Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(1));
845        // Disjoint fields should not overlap
846        assert!(!xa.overlaps(&xb));
847    }
848
849    #[test]
850    fn test_same_field_conflicts() {
851        let xa1 = Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(0));
852        let xa2 = Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(0));
853        assert!(xa1.conflicts_with(&xa2));
854    }
855
856    #[test]
857    fn test_different_locals_no_conflict() {
858        let x = Place::Local(SlotId(0));
859        let y = Place::Local(SlotId(1));
860        assert!(!x.conflicts_with(&y));
861    }
862
863    #[test]
864    fn test_parent_child_conflict() {
865        let x = Place::Local(SlotId(0));
866        let xa = Place::Field(Box::new(Place::Local(SlotId(0))), FieldIdx(0));
867        assert!(x.conflicts_with(&xa));
868        assert!(xa.conflicts_with(&x));
869    }
870}