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}