//! MIR lowering: AST -> MIR.
//!
//! Converts Shape AST function bodies into MIR basic blocks.
//! This is the bridge between parsing and borrow analysis.
//!
//! ## Module structure
//!
//! - [`mod.rs`](self) -- Public API (`lower_function`, `lower_function_detailed`,
//! `compute_mutability_errors`), `MirBuilder` struct and its state machine.
//! - [`expr`] -- Expression lowering (`lower_expr_to_temp` and its many helpers).
//! - [`stmt`] -- Statement lowering (variable decls, assignments, control flow,
//! pattern destructuring).
//! - [`helpers`] -- Shared utilities: generic container store emission, operand
//! collection, place projection, type inference from expressions.
mod expr;
mod helpers;
mod stmt;
use super::types::*;
use crate::mir::analysis::MutabilityError;
use shape_ast::ast::{self, Span, Statement};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy)]
pub(super) struct MirLoopContext {
pub(super) break_block: BasicBlockId,
pub(super) continue_block: BasicBlockId,
pub(super) break_value_slot: Option<SlotId>,
}
#[derive(Debug, Clone)]
struct TaskBoundaryCaptureScope {
outer_locals_cutoff: u16,
operands: Vec<Operand>,
}
#[derive(Debug, Clone)]
struct MirLocalRecord {
name: String,
type_info: LocalTypeInfo,
binding_info: Option<LoweredBindingInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoweredBindingInfo {
pub slot: SlotId,
pub name: String,
pub declaration_span: Span,
pub enforce_immutable_assignment: bool,
pub is_explicit_let: bool,
pub is_const: bool,
pub initialization_point: Option<Point>,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct BindingMetadata {
declaration_span: Span,
enforce_immutable_assignment: bool,
is_explicit_let: bool,
is_const: bool,
}
/// Builder for constructing a MIR function from AST.
pub struct MirBuilder {
/// Name of the function being built.
name: String,
/// Completed basic blocks.
blocks: Vec<BasicBlock>,
/// Statements for the current (in-progress) basic block.
current_stmts: Vec<MirStatement>,
/// ID of the current basic block.
pub(super) current_block: BasicBlockId,
/// Whether the current block has already been terminated and stored.
current_block_finished: bool,
/// Next block ID to allocate.
next_block_id: u32,
/// Next local slot to allocate.
next_local: u16,
/// Dedicated return slot used by explicit `return` statements.
return_slot: SlotId,
/// Next program point.
next_point: u32,
/// Next loan ID.
next_loan: u32,
/// Local variable name -> slot mapping.
locals: Vec<MirLocalRecord>,
/// Active local name -> slot mapping for place resolution.
local_slots: HashMap<String, SlotId>,
/// Stable field indices for property-place lowering.
field_indices: HashMap<String, FieldIdx>,
/// Next field index to allocate.
next_field_idx: u16,
/// Parameter slots.
param_slots: Vec<SlotId>,
/// Per-parameter reference kind, aligned with `param_slots`.
param_reference_kinds: Vec<Option<BorrowKind>>,
/// Named-local shadowing stack for lexical scopes.
scope_bindings: Vec<Vec<(String, Option<SlotId>)>>,
/// Active loop control-flow targets.
loop_contexts: Vec<MirLoopContext>,
/// Active task-boundary capture scopes for async lowering.
task_boundary_capture_scopes: Vec<TaskBoundaryCaptureScope>,
/// Nesting depth of `async scope` blocks -- nonzero means structured concurrency.
pub(super) async_scope_depth: u32,
/// Exit block for the enclosing function.
exit_block: Option<BasicBlockId>,
/// Function span.
span: Span,
/// Spans where lowering had to fall back to placeholder/Nop handling.
/// Empty means clean lowering with no fallbacks.
fallback_spans: Vec<Span>,
/// Per-local container-kind track mirroring the bytecode compiler's
/// `mut_self_container_locals` (ADR-006 §2.7.27 / W17-mutation-writeback).
/// Populated at let-binding time when the initializer is a recognised
/// COW-container ctor (`Set()` / `HashMap()` / `Deque()` /
/// `PriorityQueue()`); consumed at `Expr::MethodCall` lowering to decide
/// whether to emit the receiver write-back assignment after the call.
mut_self_container_locals:
HashMap<SlotId, crate::compiler::mutation_writeback::ContainerKind>,
/// Per-slot user-struct type name for slots produced by
/// `Expr::StructLiteral { type_name, .. }` lowering — ADR-006 §2.7.5
/// producing-site classification, Phase 3 cluster-0 Round 13 T1' gap 1
/// closure. Threaded into `MirFunction.local_struct_type_names` at
/// finalization; consumed at conduit-time by the trait-method
/// return-kind classifier to map receiver slot → struct type name →
/// trait method declared return ConcreteType.
local_struct_type_names: HashMap<SlotId, String>,
/// Per-slot empty-typed-array element ConcreteType — ADR-006 §2.7.5
/// stamp-at-compile-time, V3-S6e-jit-specialized-vec-map-aggregate-
/// classify (Phase 3 cluster-0+1 Wave 3, 2026-05-16). Populated at
/// `lower_var_decl` for `let mut name: Array<C> = []` bindings where
/// `C` is a `concrete_type_from_annotation`-resolvable element type.
/// Threaded into `MirFunction.local_typed_array_element_types`.
local_typed_array_element_types: HashMap<SlotId, shape_value::v2::ConcreteType>,
/// Per-slot declared narrow-integer scalar `ConcreteType` — ADR-006
/// §2.7.5 stamp-at-compile-time, R5c-2-β-γ (c) jit-narrow-wrap.
/// Populated at `lower_var_decl` for `let`-bindings whose type
/// annotation resolves to `i8`/`i16`/`i32`/`u8`/`u16`/`u32`. Threaded
/// into `MirFunction.local_declared_scalar_types`.
local_declared_scalar_types: HashMap<SlotId, shape_value::v2::ConcreteType>,
}
#[derive(Debug)]
pub struct MirLoweringResult {
pub mir: MirFunction,
pub had_fallbacks: bool,
/// Spans where lowering fell back to placeholder handling.
/// Used for span-granular error filtering in partial-authority mode.
pub fallback_spans: Vec<Span>,
pub binding_infos: Vec<LoweredBindingInfo>,
/// Reverse map from field index -> field name (inverted from `field_indices`).
pub field_names: HashMap<FieldIdx, String>,
/// All named locals (params + bindings), excluding `__mir_*` temporaries.
/// Used by callee summary filtering to detect local-name shadows.
pub all_local_names: HashSet<String>,
}
// ---------------------------------------------------------------------------
// MirBuilder -- block and state machine management
// ---------------------------------------------------------------------------
impl MirBuilder {
pub fn new(name: String, span: Span) -> Self {
let return_slot = SlotId(0);
MirBuilder {
name,
blocks: Vec::new(),
current_stmts: Vec::new(),
current_block: BasicBlockId(0),
current_block_finished: false,
next_block_id: 1,
next_local: 1,
return_slot,
next_point: 0,
next_loan: 0,
locals: vec![MirLocalRecord {
name: "__mir_return".to_string(),
type_info: LocalTypeInfo::Unknown,
binding_info: None,
}],
local_slots: HashMap::new(),
field_indices: HashMap::new(),
next_field_idx: 0,
param_slots: Vec::new(),
param_reference_kinds: Vec::new(),
scope_bindings: vec![Vec::new()],
loop_contexts: Vec::new(),
task_boundary_capture_scopes: Vec::new(),
async_scope_depth: 0,
exit_block: None,
span,
fallback_spans: Vec::new(),
mut_self_container_locals: HashMap::new(),
local_struct_type_names: HashMap::new(),
local_typed_array_element_types: HashMap::new(),
local_declared_scalar_types: HashMap::new(),
}
}
/// Record the user-struct type name for a slot produced by an
/// `Expr::StructLiteral { type_name, .. }` lowering. ADR-006 §2.7.5
/// producing-site classification, Phase 3 cluster-0 Round 13 T1' gap 1
/// closure.
///
/// Read at conduit-time (compiler `infer_top_level_concrete_types_from_mir_with_returns`
/// and JIT `mir_compiler/types::infer_slot_kinds_with_concrete`) to map
/// the receiver slot of a trait-method dispatch call (e.g. `t.name()`
/// where `t = X {}`) to the struct type name `"X"`, which then resolves
/// the trait method declared return ConcreteType via the
/// `find_default_trait_impl_for_type_method` chain.
pub(super) fn record_local_struct_type_name(&mut self, slot: SlotId, type_name: String) {
self.local_struct_type_names.insert(slot, type_name);
}
/// Record the empty-typed-array element ConcreteType for a slot
/// produced by `let mut name: Array<C> = []` lowering. ADR-006
/// §2.7.5 stamp-at-compile-time — V3-S6e-jit-specialized-vec-map-
/// aggregate-classify (Phase 3 cluster-0+1 Wave 3, 2026-05-16).
///
/// Read at conduit-time
/// (`compiler/helpers.rs::infer_top_level_concrete_types_from_mir_with_
/// resolvers`) to stamp `concrete_types[slot] = Array(elem)` so the
/// JIT-MIR consumer's v2-fast-path (`statements.rs::v2_typed_array_
/// elem_kind`) activates for the empty-Aggregate site that would
/// otherwise short-circuit through `emit_container_store_if_needed`
/// (helpers.rs:128-130) without producing an `ArrayStore` statement.
pub(super) fn record_local_typed_array_element_type(
&mut self,
slot: SlotId,
elem: shape_value::v2::ConcreteType,
) {
self.local_typed_array_element_types.insert(slot, elem);
}
/// Record the declared narrow-integer scalar `ConcreteType` for a slot
/// produced by a `let`-binding with an `i8`/`i16`/`i32`/`u8`/`u16`/`u32`
/// type annotation. ADR-006 §2.7.5 stamp-at-compile-time — R5c-2-β-γ
/// (c) jit-narrow-wrap.
///
/// Read at conduit-time
/// (`compiler/helpers.rs::infer_top_level_concrete_types_from_mir_with_
/// resolvers`) to stamp `concrete_types[slot]` with the proven narrow
/// width, so the JIT declares the slot at the matching Cranelift width
/// and lowers arithmetic that wraps on overflow — matching the
/// bytecode VM's `AddI32`/`AddTyped` truncating opcodes.
pub(super) fn record_local_declared_scalar_type(
&mut self,
slot: SlotId,
ct: shape_value::v2::ConcreteType,
) {
self.local_declared_scalar_types.insert(slot, ct);
}
/// Look up the declared scalar `ConcreteType` recorded for a binding
/// slot, if any. Used by the u64-literal kind-inference pass in
/// `lower_expr_to_temp`'s `Expr::BinaryOp` arm to detect a `u64`-typed
/// sibling operand. Returns `None` when the slot has no declared
/// width-carrying scalar annotation.
pub(super) fn lookup_local_declared_scalar_type(
&self,
slot: SlotId,
) -> Option<&shape_value::v2::ConcreteType> {
self.local_declared_scalar_types.get(&slot)
}
/// Record a recognized COW-container kind for a binding slot. Called
/// from let-binding lowering when the initializer is a known ctor
/// (`Set()` / `HashMap()` / `Deque()` / `PriorityQueue()`). Read at
/// `Expr::MethodCall` lowering to gate the mut-self write-back per
/// ADR-006 §2.7.27.
pub(super) fn record_mut_self_container_local(
&mut self,
slot: SlotId,
kind: crate::compiler::mutation_writeback::ContainerKind,
) {
self.mut_self_container_locals.insert(slot, kind);
}
/// Look up a recognized COW-container kind for a binding slot. Returns
/// `None` when the binding is not a tracked container (the standard
/// no-writeback path).
pub(super) fn lookup_mut_self_container_local(
&self,
slot: SlotId,
) -> Option<crate::compiler::mutation_writeback::ContainerKind> {
self.mut_self_container_locals.get(&slot).copied()
}
/// Allocate a new local variable slot.
pub fn alloc_local(&mut self, name: String, type_info: LocalTypeInfo) -> SlotId {
self.alloc_local_with_binding(name, type_info, None)
}
pub(super) fn alloc_local_binding(
&mut self,
name: String,
type_info: LocalTypeInfo,
binding_metadata: BindingMetadata,
) -> SlotId {
self.alloc_local_with_binding(name, type_info, Some(binding_metadata))
}
fn alloc_local_with_binding(
&mut self,
name: String,
type_info: LocalTypeInfo,
binding_metadata: Option<BindingMetadata>,
) -> SlotId {
self.alloc_local_with_binding_options(name, type_info, binding_metadata, true)
}
/// Allocate a local slot but defer the name->slot binding. Returns the
/// slot id and the name; the caller must invoke `bind_named_local` with
/// the returned (name, slot) pair after any pending name lookups have
/// resolved against the OUTER scope (e.g. for the `let x = x` shadow
/// pattern in `lower_var_decl` per Phase 4b Round 5c-2-α Vec.reduce
/// fold-state JIT divergence fix 2026-05-19).
pub(super) fn alloc_local_with_binding_deferred(
&mut self,
name: String,
type_info: LocalTypeInfo,
binding_metadata: Option<BindingMetadata>,
) -> SlotId {
self.alloc_local_with_binding_options(name, type_info, binding_metadata, false)
}
fn alloc_local_with_binding_options(
&mut self,
name: String,
type_info: LocalTypeInfo,
binding_metadata: Option<BindingMetadata>,
bind_now: bool,
) -> SlotId {
let slot = SlotId(self.next_local);
self.next_local += 1;
let binding_info = binding_metadata.map(|binding_metadata| LoweredBindingInfo {
slot,
name: name.clone(),
declaration_span: binding_metadata.declaration_span,
enforce_immutable_assignment: binding_metadata.enforce_immutable_assignment,
is_explicit_let: binding_metadata.is_explicit_let,
is_const: binding_metadata.is_const,
initialization_point: None,
});
self.locals.push(MirLocalRecord {
name,
type_info,
binding_info,
});
if bind_now {
if let Some(local) = self.locals.last()
&& !local.name.starts_with("__mir_")
{
self.bind_named_local(local.name.clone(), slot);
}
}
slot
}
/// Public: bind a name to a previously-allocated slot. Used after
/// `alloc_local_with_binding_deferred` to register the name once the
/// initializer expression has been lowered against the OUTER scope.
pub(super) fn bind_named_local_pub(&mut self, name: String, slot: SlotId) {
self.bind_named_local(name, slot);
}
/// Allocate a temporary local slot that should not participate in name resolution.
pub fn alloc_temp(&mut self, type_info: LocalTypeInfo) -> SlotId {
let name = format!("__mir_tmp{}", self.next_local);
self.alloc_local(name, type_info)
}
/// Register a parameter slot.
fn add_param(
&mut self,
name: String,
type_info: LocalTypeInfo,
reference_kind: Option<BorrowKind>,
binding_metadata: Option<BindingMetadata>,
) -> SlotId {
let slot = self.alloc_local_with_binding(name, type_info, binding_metadata);
self.param_slots.push(slot);
self.param_reference_kinds.push(reference_kind);
slot
}
/// Look up the current slot for a named local.
pub fn lookup_local(&self, name: &str) -> Option<SlotId> {
self.local_slots.get(name).copied()
}
pub fn visible_named_locals(&self) -> Vec<String> {
self.local_slots
.keys()
.filter(|name| !name.starts_with("__mir_"))
.cloned()
.collect()
}
/// Get or allocate a stable field index for a property name.
pub fn field_idx(&mut self, property: &str) -> FieldIdx {
if let Some(idx) = self.field_indices.get(property).copied() {
return idx;
}
let idx = FieldIdx(self.next_field_idx);
self.next_field_idx += 1;
self.field_indices.insert(property.to_string(), idx);
idx
}
pub fn return_slot(&self) -> SlotId {
self.return_slot
}
pub fn set_exit_block(&mut self, block: BasicBlockId) {
self.exit_block = Some(block);
}
pub fn exit_block(&self) -> BasicBlockId {
self.exit_block
.expect("MIR builder exit block should be initialized before lowering")
}
pub fn push_scope(&mut self) {
self.scope_bindings.push(Vec::new());
}
pub fn pop_scope(&mut self) {
if self.scope_bindings.len() <= 1 {
return;
}
if let Some(bindings) = self.scope_bindings.pop() {
for (name, previous_slot) in bindings.into_iter().rev() {
if let Some(slot) = previous_slot {
self.local_slots.insert(name, slot);
} else {
self.local_slots.remove(&name);
}
}
}
}
fn bind_named_local(&mut self, name: String, slot: SlotId) {
if let Some(scope) = self.scope_bindings.last_mut()
&& !scope.iter().any(|(existing, _)| existing == &name)
{
scope.push((name.clone(), self.local_slots.get(&name).copied()));
}
self.local_slots.insert(name, slot);
}
pub fn mark_fallback(&mut self) {
// Legacy: called without a span. Use the current function span as fallback.
self.fallback_spans.push(self.span);
}
pub fn mark_fallback_at(&mut self, span: Span) {
self.fallback_spans.push(span);
}
pub fn had_fallbacks(&self) -> bool {
!self.fallback_spans.is_empty()
}
pub fn push_loop(
&mut self,
break_block: BasicBlockId,
continue_block: BasicBlockId,
break_value_slot: Option<SlotId>,
) {
self.loop_contexts.push(MirLoopContext {
break_block,
continue_block,
break_value_slot,
});
}
pub fn pop_loop(&mut self) {
self.loop_contexts.pop();
}
pub(super) fn current_loop(&self) -> Option<MirLoopContext> {
self.loop_contexts.last().copied()
}
pub fn push_task_boundary_capture_scope(&mut self) {
self.task_boundary_capture_scopes
.push(TaskBoundaryCaptureScope {
outer_locals_cutoff: self.next_local,
operands: Vec::new(),
});
}
pub fn pop_task_boundary_capture_scope(&mut self) -> Vec<Operand> {
self.task_boundary_capture_scopes
.pop()
.map(|scope| scope.operands)
.unwrap_or_default()
}
pub fn record_task_boundary_operand(&mut self, operand: Operand) {
for scope in &mut self.task_boundary_capture_scopes {
if !helpers::operand_crosses_task_boundary(scope.outer_locals_cutoff, &operand) {
continue;
}
if !scope.operands.contains(&operand) {
scope.operands.push(operand.clone());
}
}
}
pub fn record_task_boundary_reference_capture(
&mut self,
reference_slot: SlotId,
borrowed_place: &Place,
) {
let reference_operand = Operand::Copy(Place::Local(reference_slot));
for scope in &mut self.task_boundary_capture_scopes {
if borrowed_place.root_local().0 >= scope.outer_locals_cutoff {
continue;
}
if !scope.operands.contains(&reference_operand) {
scope.operands.push(reference_operand.clone());
}
}
}
/// Allocate a new program point.
pub fn next_point(&mut self) -> Point {
let p = Point(self.next_point);
self.next_point += 1;
p
}
/// Allocate a new loan ID.
pub fn next_loan(&mut self) -> LoanId {
let l = LoanId(self.next_loan);
self.next_loan += 1;
l
}
/// Create a new basic block and return its ID.
pub fn new_block(&mut self) -> BasicBlockId {
let id = BasicBlockId(self.next_block_id);
self.next_block_id += 1;
id
}
/// Push a statement into the current block.
pub fn push_stmt(&mut self, kind: StatementKind, span: Span) -> Point {
let point = self.next_point();
self.current_stmts.push(MirStatement { kind, span, point });
point
}
pub fn record_binding_initialization(&mut self, slot: SlotId, point: Point) {
if let Some(local) = self.locals.get_mut(slot.0 as usize)
&& let Some(binding_info) = local.binding_info.as_mut()
{
binding_info.initialization_point = Some(point);
}
}
/// Finish the current block with a terminator and switch to a new block.
pub fn finish_block(&mut self, terminator_kind: TerminatorKind, span: Span) {
let block = BasicBlock {
id: self.current_block,
statements: std::mem::take(&mut self.current_stmts),
terminator: Terminator {
kind: terminator_kind,
span,
},
};
self.blocks.push(block);
self.current_block_finished = true;
}
/// Start building a new block (after finishing the previous one).
pub fn start_block(&mut self, id: BasicBlockId) {
self.current_block = id;
self.current_stmts.clear();
self.current_block_finished = false;
}
/// Emit a function call as a block terminator. Finishes current block
/// with TerminatorKind::Call and starts a continuation block.
pub fn emit_call(
&mut self,
func: Operand,
args: Vec<Operand>,
destination: Place,
span: Span,
) {
let next_bb = self.new_block();
self.finish_block(
TerminatorKind::Call {
func,
args,
destination,
next: next_bb,
},
span,
);
self.start_block(next_bb);
}
/// Finalize and produce the MIR function.
pub fn build(self) -> MirLoweringResult {
let local_types = self
.locals
.iter()
.map(|local| local.type_info.clone())
.collect();
let binding_infos = self
.locals
.iter()
.filter_map(|local| local.binding_info.clone())
.collect();
let field_names: HashMap<FieldIdx, String> = self
.field_indices
.iter()
.map(|(name, &idx)| (idx, name.clone()))
.collect();
// Sort blocks by ID so that MirFunction::block(id) can index by id.0
let mut blocks = self.blocks;
blocks.sort_by_key(|b| b.id.0);
let had_fallbacks = !self.fallback_spans.is_empty();
let fallback_spans = self.fallback_spans;
let all_local_names: HashSet<String> = self
.locals
.iter()
.filter(|l| !l.name.starts_with("__mir_"))
.map(|l| l.name.clone())
.collect();
MirLoweringResult {
mir: MirFunction {
name: self.name,
blocks,
num_locals: self.next_local,
param_slots: self.param_slots,
param_reference_kinds: self.param_reference_kinds,
local_types,
span: self.span,
field_name_table: field_names.clone(),
local_struct_type_names: self.local_struct_type_names,
local_typed_array_element_types: self.local_typed_array_element_types,
local_declared_scalar_types: self.local_declared_scalar_types,
},
had_fallbacks,
fallback_spans,
binding_infos,
field_names,
all_local_names,
}
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
pub(super) fn immutable_binding_metadata(
declaration_span: Span,
is_explicit_let: bool,
is_const: bool,
) -> BindingMetadata {
BindingMetadata {
declaration_span,
enforce_immutable_assignment: true,
is_explicit_let,
is_const,
}
}
/// Lower a function body (list of statements) into MIR.
pub fn lower_function_detailed(
name: &str,
params: &[ast::FunctionParameter],
body: &[Statement],
span: Span,
) -> MirLoweringResult {
let mut builder = MirBuilder::new(name.to_string(), span);
// Register parameters
for param in params {
let type_info = if param.is_reference {
LocalTypeInfo::NonCopy // references are always tracked
} else {
LocalTypeInfo::Unknown // will be resolved during analysis
};
let reference_kind = if param.is_mut_reference {
Some(BorrowKind::Exclusive)
} else if param.is_reference {
Some(BorrowKind::Shared)
} else {
None
};
let binding_metadata = if param.is_const {
Some(immutable_binding_metadata(param.span(), false, true))
} else if matches!(reference_kind, Some(BorrowKind::Shared)) {
Some(immutable_binding_metadata(param.span(), false, false))
} else {
None
};
if let Some(param_name) = param.simple_name() {
let slot = builder.add_param(
param_name.to_string(),
type_info,
reference_kind,
binding_metadata,
);
// cluster-2-closure-wave-1-iter-statemachine (2026-05-16):
// seed `local_typed_array_element_types` from a typed-array
// param annotation (`self: Vec<C>` / `xs: Array<C>` / etc.).
// The conduit producer at `crates/shape-vm/src/compiler/
// helpers.rs::infer_top_level_concrete_types_from_mir_with_
// resolvers` stamps `concrete_types[slot] = Array(elem)` for
// the param from this map (reusing the V3-S6e empty-array
// stamping pass — `concrete_type_from_annotation` resolves
// `Vec<C>` and `Array<C>` annotations).
//
// Load-bearing for the index-counter iter state-machine
// emitted by `lower_for_expr` / `lower_for_loop`
// generic-iterator branches at this checkpoint: when the
// iter is a typed-array receiver (e.g. `for item in self`
// inside the post-monomorphization specialized `Vec.map<T,
// U>` body where `self: Vec<T>` substitutes to `self:
// Array<I64>`), the slot-move propagation pass in the
// conduit producer flows `Array(I64)` from the param slot to
// the iter_slot, and the JIT-MIR `v2_typed_array_elem_kind`
// fast path fires for the `len()` Call terminator and the
// `Place::Index` per-iteration read. Without this seed, the
// trampoline gets receiver_kind=UInt64 (the §2.7.5 carrier
// fallback) and the `len` dispatch returns 0 (the loop body
// executes zero times).
//
// ADR-006 §2.7.5 stamp-at-compile-time: the param annotation
// IS the proof of the receiver's ConcreteType at compile
// time. No tag-bit decode, no Bool-default — params without
// typed-array annotations leave no entry in the map and the
// conduit producer leaves `concrete_types[slot]` as `Void`
// per §2.7.5.1.
if let Some(annotation) = param.type_annotation.as_ref() {
if let Some(shape_value::v2::ConcreteType::Array(elem)) =
crate::compiler::v2_map_emission::concrete_type_from_annotation(annotation)
{
builder.record_local_typed_array_element_type(slot, *elem);
}
}
} else {
let slot = builder.add_param(
format!("__mir_param{}", builder.param_slots.len()),
type_info,
reference_kind,
None,
);
stmt::lower_destructure_bindings_from_place(
&mut builder,
¶m.pattern,
&Place::Local(slot),
param.span(),
binding_metadata,
);
}
}
// Create the exit block
let exit_block = builder.new_block();
builder.set_exit_block(exit_block);
// Lower body statements
stmt::lower_statements(&mut builder, body, exit_block);
// If current block hasn't been finished (no explicit return), emit goto exit
if !builder.current_block_finished {
builder.finish_block(TerminatorKind::Goto(exit_block), span);
}
// Create exit block with Return terminator
builder.start_block(exit_block);
builder.finish_block(TerminatorKind::Return, span);
builder.build()
}
/// Lower a function body (list of statements) into MIR.
pub fn lower_function(
name: &str,
params: &[ast::FunctionParameter],
body: &[Statement],
span: Span,
) -> MirFunction {
lower_function_detailed(name, params, body, span).mir
}
pub fn compute_mutability_errors(lowering: &MirLoweringResult) -> Vec<MutabilityError> {
let tracked_bindings: HashMap<SlotId, &LoweredBindingInfo> = lowering
.binding_infos
.iter()
.filter(|binding| binding.enforce_immutable_assignment)
.map(|binding| (binding.slot, binding))
.collect();
let mut errors = Vec::new();
for block in &lowering.mir.blocks {
for stmt in &block.statements {
let StatementKind::Assign(place, _) = &stmt.kind else {
continue;
};
let root = place.root_local();
let Some(binding) = tracked_bindings.get(&root) else {
continue;
};
let is_declaration_init = matches!(place, Place::Local(slot) if *slot == root)
&& binding.initialization_point == Some(stmt.point);
if is_declaration_init {
continue;
}
errors.push(MutabilityError {
span: stmt.span,
variable_name: binding.name.clone(),
declaration_span: binding.declaration_span,
is_explicit_let: binding.is_explicit_let,
is_const: binding.is_const,
});
}
}
errors
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::mir::analysis::BorrowErrorKind;
use crate::mir::cfg::ControlFlowGraph;
use crate::mir::liveness;
use crate::mir::solver;
use shape_ast::ast::{self, DestructurePattern, Expr, OwnershipModifier, VarKind};
fn span() -> Span {
Span { start: 0, end: 1 }
}
fn lower_parsed_function(code: &str) -> MirLoweringResult {
let program = shape_ast::parser::parse_program(code).expect("parse failed");
let func = match &program.items[0] {
ast::Item::Function(func, _) => func,
_ => panic!("expected function item"),
};
lower_function_detailed(&func.name, &func.params, &func.body, func.name_span)
}
#[test]
fn test_lower_empty_function() {
let mir = lower_function("empty", &[], &[], span());
assert_eq!(mir.name, "empty");
assert!(mir.blocks.len() >= 2); // entry + exit
assert_eq!(mir.num_locals, 1);
}
#[test]
fn test_lower_simple_var_decl() {
let body = vec![Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(42), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
)];
let mir = lower_function("test", &[], &body, span());
assert!(mir.num_locals >= 1); // at least x + temp
// Should have at least 2 blocks (entry + exit)
assert!(mir.blocks.len() >= 2);
}
#[test]
fn test_compute_mutability_errors_ignores_binding_initializer() {
let lowering = lower_parsed_function(
r#"
function keep() {
let x = 1
x
}
"#,
);
let errors = compute_mutability_errors(&lowering);
assert!(
errors.is_empty(),
"declaration initializer should not be reported as a mutability error: {:?}",
errors
);
}
#[test]
fn test_compute_mutability_errors_flags_immutable_let_reassignment() {
let lowering = lower_parsed_function(
r#"
function mutate() {
let x = 1
x = 2
x
}
"#,
);
let errors = compute_mutability_errors(&lowering);
assert_eq!(
errors.len(),
1,
"expected one mutability error, got {errors:?}"
);
assert_eq!(errors[0].variable_name, "x");
assert!(errors[0].is_explicit_let);
}
#[test]
fn test_compute_mutability_errors_flags_const_reassignment() {
let lowering = lower_parsed_function(
r#"
function mutate() {
const x = 1
x = 2
x
}
"#,
);
let errors = compute_mutability_errors(&lowering);
assert_eq!(
errors.len(),
1,
"expected one mutability error, got {errors:?}"
);
assert_eq!(errors[0].variable_name, "x");
assert!(errors[0].is_const);
}
#[test]
fn test_compute_mutability_errors_flags_shared_ref_param_write() {
let lowering = lower_parsed_function(
r#"
function mutate(&x) {
x = 2
x
}
"#,
);
let errors = compute_mutability_errors(&lowering);
assert_eq!(
errors.len(),
1,
"expected one mutability error, got {errors:?}"
);
assert_eq!(errors[0].variable_name, "x");
assert!(!errors[0].is_explicit_let);
}
#[test]
fn test_compute_mutability_errors_flags_const_param_write() {
let lowering = lower_parsed_function(
r#"
function mutate(const x) {
x = 2
x
}
"#,
);
let errors = compute_mutability_errors(&lowering);
assert_eq!(
errors.len(),
1,
"expected one mutability error, got {errors:?}"
);
assert_eq!(errors[0].variable_name, "x");
assert!(errors[0].is_const);
}
#[test]
fn test_lower_with_liveness() {
// let x = 1; let y = x; (x live after first stmt, dead after second)
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(
ast::Literal::String("hi".to_string()),
span(),
)),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("y".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("x".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("kept".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("shared".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test", &[], &body, span());
let cfg = ControlFlowGraph::build(&mir);
let _liveness = liveness::compute_liveness(&mir, &cfg);
// The MIR lowers and liveness computes without panic
}
#[test]
fn test_lower_reference_to_identifier_borrows_original_local() {
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(
ast::Literal::String("hi".to_string()),
span(),
)),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("r".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::Identifier("x".to_string(), span())),
is_mutable: false,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test", &[], &body, span());
let borrow_place = mir
.blocks
.iter()
.flat_map(|block| block.statements.iter())
.find_map(|stmt| match &stmt.kind {
StatementKind::Assign(_, Rvalue::Borrow(_, place)) => Some(place.clone()),
_ => None,
})
.expect("expected borrow statement");
assert_eq!(borrow_place, Place::Local(SlotId(1)));
}
#[test]
fn test_lowered_local_borrow_conflict_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: true,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(1), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("shared".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::Identifier("x".to_string(), span())),
is_mutable: false,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("exclusive".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::Identifier("x".to_string(), span())),
is_mutable: true,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::Return(Some(Expr::Identifier("shared".to_string(), span())), span()),
];
let mir = lower_function("test", &[], &body, span());
let analysis = solver::analyze(&mir, &Default::default());
assert!(
analysis
.errors
.iter()
.any(|error| error.kind == BorrowErrorKind::ConflictSharedExclusive),
"expected shared/exclusive conflict, got {:?}",
analysis.errors
);
}
#[test]
fn test_lowered_property_borrows_preserve_disjoint_places() {
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: true,
pattern: DestructurePattern::Identifier("pair".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(0), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("left".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::PropertyAccess {
object: Box::new(Expr::Identifier("pair".to_string(), span())),
property: "left".to_string(),
optional: false,
span: span(),
}),
is_mutable: true,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("right".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::PropertyAccess {
object: Box::new(Expr::Identifier("pair".to_string(), span())),
property: "right".to_string(),
optional: false,
span: span(),
}),
is_mutable: true,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("kept".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("shared".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test", &[], &body, span());
let analysis = solver::analyze(&mir, &Default::default());
assert!(
analysis.errors.is_empty(),
"disjoint field borrows should not conflict, got {:?}",
analysis.errors
);
}
#[test]
fn test_lowered_write_while_borrowed_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: true,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(1), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("shared".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::Identifier("x".to_string(), span())),
is_mutable: false,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::Assignment(
ast::Assignment {
pattern: DestructurePattern::Identifier("x".to_string(), span()),
value: Expr::Literal(ast::Literal::Int(2), span()),
},
span(),
),
Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
];
let mir = lower_function("test", &[], &body, span());
let analysis = solver::analyze(&mir, &Default::default());
assert!(
analysis
.errors
.iter()
.any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed),
"expected write-while-borrowed error, got {:?}",
analysis.errors
);
}
#[test]
fn test_lowered_read_while_exclusive_borrow_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: true,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(1), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("exclusive".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::Identifier("x".to_string(), span())),
is_mutable: true,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("copy".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("x".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::Expression(Expr::Identifier("exclusive".to_string(), span()), span()),
];
let mir = lower_function("test", &[], &body, span());
let analysis = solver::analyze(&mir, &Default::default());
assert!(
analysis
.errors
.iter()
.any(|error| error.kind == BorrowErrorKind::ReadWhileExclusivelyBorrowed),
"expected read-while-exclusive error, got {:?}",
analysis.errors
);
}
#[test]
fn test_lowered_returned_ref_alias_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(1), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("r".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::Identifier("x".to_string(), span())),
is_mutable: false,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("alias".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("r".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::Return(Some(Expr::Identifier("alias".to_string(), span())), span()),
];
let mir = lower_function("test", &[], &body, span());
let analysis = solver::analyze(&mir, &Default::default());
assert!(
analysis
.errors
.iter()
.any(|error| error.kind == BorrowErrorKind::ReferenceEscape),
"expected reference-escape error, got {:?}",
analysis.errors
);
}
#[test]
fn test_lowered_array_direct_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let arr = [&x]
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_array_indirect_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let r = &x
let arr = [r]
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_object_direct_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let obj = { value: &x }
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_object_indirect_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let r = &x
let obj = { value: r }
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_struct_direct_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let point = Point { value: &x }
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_struct_indirect_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let r = &x
let point = Point { value: r }
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_enum_tuple_direct_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let value = Maybe::Some(&x)
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_enum_tuple_indirect_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let r = &x
let value = Maybe::Some(r)
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_enum_struct_direct_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let value = Maybe::Err { code: &x }
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_enum_struct_indirect_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let x = 1
let r = &x
let value = Maybe::Err { code: r }
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_use_after_explicit_move_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(
ast::Literal::String("hi".to_string()),
span(),
)),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("y".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("x".to_string(), span())),
ownership: OwnershipModifier::Move,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("z".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("x".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test", &[], &body, span());
let analysis = solver::analyze(&mir, &Default::default());
assert!(
analysis
.errors
.iter()
.any(|error| error.kind == BorrowErrorKind::UseAfterMove),
"expected use-after-move error, got {:?}",
analysis.errors
);
}
#[test]
fn test_lowered_while_expr_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let mut x = 1
let y = while true {
let shared = &x
x = 2
shared
0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_for_expr_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test(items) {
let mut x = 1
let y = for item in items {
let shared = &x
x = 2
shared
0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_loop_expr_break_value_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test() {
let mut x = 1
let y = loop {
let shared = &x
x = 2
shared
break 0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_continue_expression_in_while_body_stays_supported() {
let lowering = lower_parsed_function(
r#"
function test(flag) {
let mut x = 1
let y = while flag {
if flag { continue } else { x }
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_match_expression_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test(flag) {
let mut x = 1
let y = match flag {
true => {
let shared = &x
x = 2
shared
0
}
_ => 0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_match_expression_identifier_guard_stays_supported() {
let lowering = lower_parsed_function(
r#"
function test(v) {
let y = match v {
x where x > 0 => x
_ => 0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_match_expression_array_pattern_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test(pair) {
let mut x = 1
let y = match pair {
[left, right] => {
let shared = &x
x = 2
shared
0
}
_ => 0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_match_expression_object_pattern_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test(obj) {
let mut x = 1
let y = match obj {
{ left: l, right: r } => {
let shared = &x
x = 2
shared
0
}
_ => 0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_match_expression_constructor_pattern_write_while_borrowed_is_visible_to_solver()
{
let lowering = lower_parsed_function(
r#"
function test(opt) {
let mut x = 1
let y = match opt {
Some(v) => {
let shared = &x
x = 2
shared
0
}
None => 0
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
/// W15.2-LANG-5 regression. `Pattern::Typed` arms in a `match`
/// expression must lower to a real discriminator — pre-fix
/// `lower_match_pattern_condition_operand` grouped `Pattern::Typed`
/// with `Identifier`/`Wildcard` (returning `None` for the condition
/// operand), so the MIR builder emitted a kind-blind
/// `TerminatorKind::Goto(body_block)` for every typed arm. Under JIT
/// the first arm always won (e.g. `match x: int | string { n: int =>
/// 100, s: string => 200 }` returned 100 for both `7` and `"hi"`).
///
/// Post-fix the lowering emits `Rvalue::TypePatternTest` per arm with
/// the producer-side stamped `TypeAnnotation`, then a `SwitchBool` on
/// the resulting Bool slot. This test pins the producer-side
/// classification by scanning the lowered MIR for the new Rvalue.
#[test]
fn test_lowered_typed_pattern_match_emits_type_pattern_test() {
use crate::mir::types::{Rvalue, StatementKind};
let lowering = lower_parsed_function(
r#"
function describe(x) {
match x {
n: int => 100
s: string => 200
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let typed_tests: Vec<&Rvalue> = lowering
.mir
.blocks
.iter()
.flat_map(|b| b.statements.iter())
.filter_map(|stmt| match &stmt.kind {
StatementKind::Assign(_, rv @ Rvalue::TypePatternTest { .. }) => Some(rv),
_ => None,
})
.collect();
assert_eq!(
typed_tests.len(),
2,
"expected one TypePatternTest per typed arm (int + string); got {} -- MIR: {:#?}",
typed_tests.len(),
lowering.mir.blocks,
);
}
/// W15.2-LANG-1 (Phase 4b, 2026-05-18) regression test. Pre-fix the
/// MIR lowering of `Pattern::Constructor` for non-trinity (user-defined)
/// enum variants returned `Some(Operand::Copy(Place::Local(scrutinee)))`
/// — the raw `Arc<TypedObjectStorage>` pointer bits — as the SwitchBool
/// condition. The JIT consumer's generic I64-truthy path then evaluated
/// the non-zero pointer non-deterministically (silently empty output for
/// `match Color::Red { Color::Red => print("red"), Color::Green => ...
/// , Color::Blue => ... }`, book snippet `enums.mdx:113`).
///
/// Post-fix the lowering emits `Rvalue::EnumDiscriminantTest` per arm
/// with the producer-side stamped (enum_name, variant_name) pair, then
/// a `SwitchBool` on the resulting Bool slot. This test pins the
/// producer-side classification by scanning the lowered MIR for the
/// new Rvalue.
#[test]
fn test_lowered_user_enum_constructor_pattern_match_emits_enum_discriminant_test() {
use crate::mir::types::{Rvalue, StatementKind};
let lowering = lower_parsed_function(
r#"
function show(c) {
match c {
Color::Red => 1
Color::Green => 2
Color::Blue => 3
}
}
"#,
);
assert!(!lowering.had_fallbacks);
let discriminant_tests: Vec<&Rvalue> = lowering
.mir
.blocks
.iter()
.flat_map(|b| b.statements.iter())
.filter_map(|stmt| match &stmt.kind {
StatementKind::Assign(_, rv @ Rvalue::EnumDiscriminantTest { .. }) => Some(rv),
_ => None,
})
.collect();
assert_eq!(
discriminant_tests.len(),
3,
"expected one EnumDiscriminantTest per Color::* arm (Red + Green + Blue); \
got {} -- MIR: {:#?}",
discriminant_tests.len(),
lowering.mir.blocks,
);
// Verify each carries the expected (enum_name, variant_name) pair
// verbatim per ADR-006 §2.7.5 stamp-at-compile-time.
let mut variant_names: Vec<&str> = discriminant_tests
.iter()
.filter_map(|rv| match rv {
Rvalue::EnumDiscriminantTest {
enum_name,
variant_name,
..
} => {
assert_eq!(
enum_name.as_deref(),
Some("Color"),
"enum_name should be stamped verbatim from `Color::...`",
);
Some(variant_name.as_str())
}
_ => None,
})
.collect();
variant_names.sort();
assert_eq!(variant_names, vec!["Blue", "Green", "Red"]);
}
#[test]
fn test_lowered_destructure_var_decl_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test(pair) {
var [left, right] = pair
let shared = &left
left = 2
shared
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_destructure_param_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test([left, right]) {
let mut left_copy = left
let shared = &left_copy
left_copy = 2
shared
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_destructure_assignment_stays_supported() {
let pair_param = ast::FunctionParameter {
pattern: DestructurePattern::Identifier("pair".to_string(), span()),
is_const: false,
is_reference: false,
is_mut_reference: false,
is_out: false,
type_annotation: None,
default_value: None,
};
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: true,
pattern: DestructurePattern::Identifier("left".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(1), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: true,
pattern: DestructurePattern::Identifier("right".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(2), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::Assignment(
ast::Assignment {
pattern: DestructurePattern::Array(vec![
DestructurePattern::Identifier("left".to_string(), span()),
DestructurePattern::Identifier("right".to_string(), span()),
]),
value: Expr::Identifier("pair".to_string(), span()),
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("shared".to_string(), span()),
type_annotation: None,
value: Some(Expr::Reference {
expr: Box::new(Expr::Identifier("left".to_string(), span())),
is_mutable: false,
span: span(),
}),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::Assignment(
ast::Assignment {
pattern: DestructurePattern::Identifier("left".to_string(), span()),
value: Expr::Literal(ast::Literal::Int(3), span()),
},
span(),
),
Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
];
let lowering = lower_function_detailed("test", &[pair_param], &body, span());
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_destructure_rest_pattern_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test(items) {
var [head, ...tail] = items
let shared = &tail
tail = items
shared
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_decomposition_pattern_write_while_borrowed_is_visible_to_solver() {
let lowering = lower_parsed_function(
r#"
function test(merged) {
var (left: {x}, right: {y}) = merged
let shared = &left
left = merged
shared
}
"#,
);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_supported_runtime_opaque_expressions_stay_supported() {
let mut overrides = std::collections::HashMap::new();
overrides.insert(
"digits".to_string(),
Expr::Literal(ast::Literal::Int(2), span()),
);
let body = vec![
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("x".to_string(), span()), type_annotation: None, value: Some(Expr::Literal(ast::Literal::Int(1), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("arr".to_string(), span()), type_annotation: None, value: Some(Expr::Array(vec![Expr::Identifier("x".to_string(), span()), Expr::Literal(ast::Literal::Int(2), span())], span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("obj".to_string(), span()), type_annotation: None, value: Some(Expr::Object(vec![ast::ObjectEntry::Field { key: "left".to_string(), value: Expr::Identifier("x".to_string(), span()), type_annotation: None }, ast::ObjectEntry::Spread(Expr::Identifier("arr".to_string(), span()))], span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("unary".to_string(), span()), type_annotation: None, value: Some(Expr::UnaryOp { op: ast::UnaryOp::Neg, operand: Box::new(Expr::Identifier("x".to_string(), span())), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("fuzzy".to_string(), span()), type_annotation: None, value: Some(Expr::FuzzyComparison { left: Box::new(Expr::Identifier("x".to_string(), span())), op: ast::operators::FuzzyOp::Equal, right: Box::new(Expr::Literal(ast::Literal::Int(1), span())), tolerance: ast::operators::FuzzyTolerance::Percentage(0.02), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("slice".to_string(), span()), type_annotation: None, value: Some(Expr::IndexAccess { object: Box::new(Expr::Identifier("arr".to_string(), span())), index: Box::new(Expr::Literal(ast::Literal::Int(0), span())), end_index: Some(Box::new(Expr::Literal(ast::Literal::Int(1), span()))), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("asserted".to_string(), span()), type_annotation: None, value: Some(Expr::TypeAssertion { expr: Box::new(Expr::Identifier("x".to_string(), span())), type_annotation: ast::TypeAnnotation::Basic("int".to_string()), meta_param_overrides: Some(overrides), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("instance".to_string(), span()), type_annotation: None, value: Some(Expr::InstanceOf { expr: Box::new(Expr::Identifier("x".to_string(), span())), type_annotation: ast::TypeAnnotation::Basic("int".to_string()), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("variant".to_string(), span()), type_annotation: None, value: Some(Expr::EnumConstructor { enum_name: "Option".into(), variant: "Some".to_string(), payload: ast::EnumConstructorPayload::Tuple(vec![Expr::Identifier("x".to_string(), span())]), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("call".to_string(), span()), type_annotation: None, value: Some(Expr::MethodCall { receiver: Box::new(Expr::Identifier("obj".to_string(), span())), method: "touch".to_string(), args: vec![Expr::Identifier("x".to_string(), span())], named_args: vec![("tail".to_string(), Expr::IndexAccess { object: Box::new(Expr::Identifier("arr".to_string(), span())), index: Box::new(Expr::Literal(ast::Literal::Int(0), span())), end_index: Some(Box::new(Expr::Literal(ast::Literal::Int(1), span()))), span: span() })], optional: false, span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("range".to_string(), span()), type_annotation: None, value: Some(Expr::Range { start: Some(Box::new(Expr::Literal(ast::Literal::Int(0), span()))), end: Some(Box::new(Expr::Identifier("x".to_string(), span()))), kind: ast::RangeKind::Exclusive, span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("contextual".to_string(), span()), type_annotation: None, value: Some(Expr::TimeframeContext { timeframe: ast::Timeframe::new(5, ast::TimeframeUnit::Minute), expr: Box::new(Expr::Identifier("x".to_string(), span())), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("using_impl".to_string(), span()), type_annotation: None, value: Some(Expr::UsingImpl { expr: Box::new(Expr::Identifier("x".to_string(), span())), impl_name: "Tracked".to_string(), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("simulation".to_string(), span()), type_annotation: None, value: Some(Expr::SimulationCall { name: "sim".to_string(), params: vec![("value".to_string(), Expr::Identifier("x".to_string(), span()))], span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("struct_lit".to_string(), span()), type_annotation: None, value: Some(Expr::StructLiteral { type_name: "Point".into(), fields: vec![("x".to_string(), Expr::Identifier("x".to_string(), span()))], span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("annotated".to_string(), span()), type_annotation: None, value: Some(Expr::Annotated { annotation: ast::Annotation { name: "trace".to_string(), args: vec![Expr::Identifier("x".to_string(), span())], span: span() }, target: Box::new(Expr::Identifier("x".to_string(), span())), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("rows".to_string(), span()), type_annotation: None, value: Some(Expr::TableRows(vec![vec![Expr::Identifier("x".to_string(), span()), Expr::Literal(ast::Literal::Int(2), span())], vec![Expr::Literal(ast::Literal::Int(3), span()), Expr::Literal(ast::Literal::Int(4), span())]], span())), ownership: OwnershipModifier::Inferred }, span()),
];
let lowering = lower_function_detailed("test", &[], &body, span());
assert!(!lowering.had_fallbacks);
}
#[test]
fn test_lowered_assignment_expr_write_while_borrowed_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: true, pattern: DestructurePattern::Identifier("x".to_string(), span()), type_annotation: None, value: Some(Expr::Literal(ast::Literal::Int(1), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("shared".to_string(), span()), type_annotation: None, value: Some(Expr::Reference { expr: Box::new(Expr::Identifier("x".to_string(), span())), is_mutable: false, span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("y".to_string(), span()), type_annotation: None, value: Some(Expr::Assign(Box::new(ast::AssignExpr { target: Box::new(Expr::Identifier("x".to_string(), span())), value: Box::new(Expr::Literal(ast::Literal::Int(2), span())) }), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::Return(Some(Expr::Identifier("shared".to_string(), span())), span()),
];
let lowering = lower_function_detailed("test", &[], &body, span());
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_property_assignment_expr_preserves_disjoint_places() {
let body = vec![
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: true, pattern: DestructurePattern::Identifier("pair".to_string(), span()), type_annotation: None, value: Some(Expr::Literal(ast::Literal::String("pair".to_string()), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("left".to_string(), span()), type_annotation: None, value: Some(Expr::Reference { expr: Box::new(Expr::PropertyAccess { object: Box::new(Expr::Identifier("pair".to_string(), span())), property: "left".to_string(), optional: false, span: span() }), is_mutable: false, span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::Expression(Expr::Assign(Box::new(ast::AssignExpr { target: Box::new(Expr::PropertyAccess { object: Box::new(Expr::Identifier("pair".to_string(), span())), property: "right".to_string(), optional: false, span: span() }), value: Box::new(Expr::Literal(ast::Literal::String("updated".to_string()), span())) }), span()), span()),
];
let lowering = lower_function_detailed("test", &[], &body, span());
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_property_assignment_direct_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
function test() {
var obj = { value: 0 }
let x = 1
obj.value = &x
0
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_property_assignment_indirect_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
function test() {
var obj = { value: 0 }
let x = 1
let r = &x
obj.value = r
0
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_index_assignment_direct_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
function test() {
var arr = [0]
let x = 1
arr[0] = &x
0
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_index_assignment_indirect_ref_escape_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
function test() {
var arr = [0]
let x = 1
let r = &x
arr[0] = r
0
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_block_expr_write_while_borrowed_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: true, pattern: DestructurePattern::Identifier("x".to_string(), span()), type_annotation: None, value: Some(Expr::Literal(ast::Literal::Int(1), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("shared".to_string(), span()), type_annotation: None, value: Some(Expr::Block(ast::BlockExpr { items: vec![ast::BlockItem::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("inner".to_string(), span()), type_annotation: None, value: Some(Expr::Reference { expr: Box::new(Expr::Identifier("x".to_string(), span())), is_mutable: false, span: span() }), ownership: OwnershipModifier::Inferred }), ast::BlockItem::Expression(Expr::Identifier("inner".to_string(), span()))] }, span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::Assignment(ast::Assignment { pattern: DestructurePattern::Identifier("x".to_string(), span()), value: Expr::Literal(ast::Literal::Int(2), span()) }, span()),
Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
];
let lowering = lower_function_detailed("test", &[], &body, span());
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_let_expr_write_while_borrowed_is_visible_to_solver() {
let body = vec![
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: true, pattern: DestructurePattern::Identifier("x".to_string(), span()), type_annotation: None, value: Some(Expr::Literal(ast::Literal::Int(1), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("shared".to_string(), span()), type_annotation: None, value: Some(Expr::Let(Box::new(ast::LetExpr { pattern: ast::Pattern::Identifier("inner".to_string()), type_annotation: None, value: Some(Box::new(Expr::Reference { expr: Box::new(Expr::Identifier("x".to_string(), span())), is_mutable: false, span: span() })), body: Box::new(Expr::Identifier("inner".to_string(), span())) }), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::Assignment(ast::Assignment { pattern: DestructurePattern::Identifier("x".to_string(), span()), value: Expr::Literal(ast::Literal::Int(2), span()) }, span()),
Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
];
let lowering = lower_function_detailed("test", &[], &body, span());
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_if_expression_with_block_branches_stays_supported() {
let block_branch = |borrow_name: &str| {
Expr::Block(ast::BlockExpr { items: vec![ast::BlockItem::Expression(Expr::Reference { expr: Box::new(Expr::Identifier(borrow_name.to_string(), span())), is_mutable: false, span: span() })] }, span())
};
let body = vec![
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: true, pattern: DestructurePattern::Identifier("x".to_string(), span()), type_annotation: None, value: Some(Expr::Literal(ast::Literal::Int(1), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("flag".to_string(), span()), type_annotation: None, value: Some(Expr::Literal(ast::Literal::Bool(true), span())), ownership: OwnershipModifier::Inferred }, span()),
Statement::VariableDecl(ast::VariableDecl { kind: VarKind::Let, is_mut: false, pattern: DestructurePattern::Identifier("shared".to_string(), span()), type_annotation: None, value: Some(Expr::Conditional { condition: Box::new(Expr::Identifier("flag".to_string(), span())), then_expr: Box::new(block_branch("x")), else_expr: Some(Box::new(block_branch("x"))), span: span() }), ownership: OwnershipModifier::Inferred }, span()),
Statement::Assignment(ast::Assignment { pattern: DestructurePattern::Identifier("x".to_string(), span()), value: Expr::Literal(ast::Literal::Int(2), span()) }, span()),
];
let lowering = lower_function_detailed("test", &[], &body, span());
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_async_let_exclusive_ref_task_boundary_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
async function test() {
let mut x = 1
async let fut = &mut x
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
}
#[test]
fn test_lowered_async_let_nested_ref_binding_task_boundary_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
async function test() {
let mut x = 1
async let fut = {
let r = &mut x
r
}
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
}
#[test]
fn test_lowered_async_let_shared_ref_task_boundary_stays_clean() {
let lowering = lower_parsed_function(r#"
async function test() {
let x = 1
async let fut = &x
await fut
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(!analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
}
#[test]
fn test_lowered_join_exclusive_ref_task_boundary_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
async function test() {
let mut x = 1
await join all {
&mut x,
2,
}
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
}
#[test]
fn test_lowered_async_scope_with_async_let_stays_supported() {
let lowering = lower_parsed_function(r#"
async function test() {
let x = 1
async scope {
async let fut = &x
await fut
}
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_closure_capture_of_reference_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
function test() {
let x = 1
let r = &x
let f = || r
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_returned_array_with_ref_still_errors() {
let lowering = lower_parsed_function(r#"
function test() {
let x = 1
let arr = [&x]
return arr
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ReferenceStoredInArray));
}
#[test]
fn test_lowered_returned_closure_with_ref_still_errors() {
let lowering = lower_parsed_function(r#"
function test() {
let x = 1
let r = &x
let f = || r
return f
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ReferenceEscapeIntoClosure));
}
#[test]
fn test_lowered_closure_capture_of_owned_value_stays_clean() {
let lowering = lower_parsed_function(r#"
function test() {
let x = 1
let f = || x
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.is_empty());
}
#[test]
fn test_lowered_list_comprehension_write_conflict_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
function test() {
let mut x = 1
let r = &x
let xs = [(x = 2) for y in [1]]
r
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_from_query_write_conflict_is_visible_to_solver() {
let lowering = lower_parsed_function(r#"
function test() {
let mut x = 1
let r = &x
let rows = from y in [1] where (x = 2) > 0 select y
r
}
"#);
assert!(!lowering.had_fallbacks);
let analysis = solver::analyze(&lowering.mir, &Default::default());
assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
}
#[test]
fn test_lowered_comptime_expr_stays_supported() {
let lowering = lower_parsed_function(r#"
function test() {
let generated = comptime {
let x = 1
}
}
"#);
assert!(!lowering.had_fallbacks);
}
#[test]
fn test_lowered_comptime_for_expr_stays_supported() {
let lowering = lower_parsed_function(r#"
function test() {
let generated = comptime for f in [1, 2] {
let y = f
}
}
"#);
assert!(!lowering.had_fallbacks);
}
#[test]
fn test_lowered_numeric_literals_preserve_values() {
// This test verifies that numeric literals are lowered to MirConstant
// with their actual values, not placeholder MirConstant::Int(0).
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("a".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(42), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("b".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Number(2.5), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("c".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Bool(true), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test", &[], &body, span());
// Collect all constants from the MIR.
let mut constants: Vec<MirConstant> = Vec::new();
for block in &mir.blocks {
for stmt in &block.statements {
if let StatementKind::Assign(_, Rvalue::Use(Operand::Constant(c))) = &stmt.kind {
constants.push(c.clone());
}
}
}
// Verify: Int(42) is present, not Int(0)
assert!(
constants.contains(&MirConstant::Int(42)),
"expected MirConstant::Int(42) in {constants:?}"
);
// Verify: Float(2.5 bits) is present, not Float(0)
assert!(
constants.contains(&MirConstant::Float(f64::to_bits(2.5))),
"expected MirConstant::Float(2.5) in {constants:?}"
);
// Verify: Bool(true) is present
assert!(
constants.contains(&MirConstant::Bool(true)),
"expected MirConstant::Bool(true) in {constants:?}"
);
}
/// W15.2-LANG-2 regression: `continue` inside a `for i in 0..N { ... }`
/// body must NOT jump directly to the loop header — it must route
/// through a dedicated `continue_target` block that performs the
/// counter increment, otherwise `continue` at idx=K leaves idx=K
/// and re-enters the body infinitely. Verified by structural MIR
/// inspection: the loop-context's `continue_block` exposed via the
/// body's `Continue` statement must NOT equal the block targeted by
/// the header's `SwitchBool { true_bb, .. }`. Empirically this same
/// invariant repaired the audit reproducer (`fundamentals/control-
/// flow.mdx:87` snippet `A__fundamentals__control-flow__07__L0087`)
/// from JIT `0\n1` (infinite-loop after first continue) to JIT
/// `0\n1\n3\n4\n5` matching VM.
#[test]
fn test_for_range_continue_routes_through_increment_block() {
let mir = lower_parsed_function(
"fn run() {\n for i in 0..3 {\n if i == 1 { continue }\n print(i)\n }\n}",
)
.mir;
// Find the header block: it has a SwitchBool terminator whose
// condition is computed by a BinaryOp::Lt comparison (the
// `counter < end` check). There is exactly one such block in
// this fixture.
let mut header_id: Option<BasicBlockId> = None;
let mut header_true_bb: Option<BasicBlockId> = None;
for block in &mir.blocks {
let has_lt = block.statements.iter().any(|s| {
matches!(
&s.kind,
StatementKind::Assign(_, Rvalue::BinaryOp(BinOp::Lt, _, _))
)
});
if has_lt {
if let TerminatorKind::SwitchBool { true_bb, .. } = &block.terminator.kind {
header_id = Some(block.id);
header_true_bb = Some(*true_bb);
break;
}
}
}
let header_id = header_id.expect("loop header block not found");
let header_true_bb = header_true_bb.expect("loop header SwitchBool true_bb not found");
// Find a block whose terminator is `Goto(target)` AND whose
// statements contain the counter advance (`x = x + 1`). The
// ForIn lowering may produce more than one block matching one
// half of this pattern, so we require BOTH halves to identify
// the `continue_target` block unambiguously.
let increment_target_id = mir
.blocks
.iter()
.find(|b| {
let goes_to_header = matches!(
&b.terminator.kind,
TerminatorKind::Goto(t) if *t == header_id
);
let increments = b.statements.iter().any(|s| {
matches!(
&s.kind,
StatementKind::Assign(_, Rvalue::BinaryOp(BinOp::Add, _, _))
)
});
goes_to_header && increments
})
.map(|b| b.id)
.expect("continue_target block (increment + Goto(header)) not found");
// The fix's invariant: the `continue` statement inside the body
// must terminate at a block (the dedicated `continue_target`)
// that is DIFFERENT from the body block (the header's true_bb)
// — that distinction is exactly what makes the increment run
// for the `continue` path.
assert_ne!(
header_true_bb, increment_target_id,
"loop body and continue_target collapsed to the same block — \
`continue` would skip the increment and infinite-loop"
);
// Sanity: the increment block must NOT equal the header itself
// (otherwise the increment runs at every iteration BEFORE the
// cond check, which is a different broken shape).
assert_ne!(
increment_target_id, header_id,
"continue_target block must not equal header (the increment \
must precede the back-jump, not the cond check)"
);
}
// Phase 4b Round 5c-2-α Vec.reduce fold-state JIT divergence
// (v0.3-gating SOUNDNESS BUG) regression tests per supervisor ratify
// 2026-05-19. Sister-class to LANG-9-spin-3-first.
//
// Root cause: MIR `lower_var_decl` allocated the new binding slot AND
// bound the name (`bind_named_local`) BEFORE lowering the initializer
// expression. For the same-name shadow pattern `let acc = acc`, the
// RHS identifier read resolved through `lookup_local("acc")` against
// the JUST-bound new slot (uninitialized!) instead of the OUTER one.
//
// Fix shape: `lower_var_decl` now allocates via
// `alloc_local_with_binding_deferred` (slot record created, name
// resolution deferred), lowers the initializer (RHS reads OUTER
// binding for the name), then registers the name via
// `bind_named_local_pub` so the new shadow becomes visible to
// subsequent statements.
//
// Surface: Vec.reduce stdlib body after Phase C closure inlining
// becomes `acc = { let acc = acc; let x = item; acc + x }` (per
// `compiler/monomorphization/substitution.rs::build_inlined_closure
// _block`). The same-name `let acc = acc` shadow tripped the MIR-side
// gap, returning the last item value instead of the folded sum.
// Original empirical reproducer (W15.2-D close): `[1,2,3,4].reduce(
// |a,b| a+b, 0)` VM=10 / JIT=4.
#[test]
fn test_shadow_with_same_name_lowers_init_against_outer_binding() {
// `let x = 1; let x = x; x` — the inner `let x = x` must read
// the OUTER x's value (1), not the inner uninitialized slot.
// Pre-fix: MIR emitted `Assign(inner_x, Use(Copy(inner_x)))` —
// self-referential, reading the new slot's default (0) instead
// of the outer slot's 1.
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(1), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("x".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test_shadow_same_name", &[], &body, span());
// Find the second VariableDecl's Assign — the RHS Operand must
// reference a DIFFERENT slot than the LHS destination (the OUTER
// x's slot, not the INNER x's slot).
let mut assign_pairs = Vec::new();
for block in mir.blocks.iter() {
for stmt in block.statements.iter() {
if let StatementKind::Assign(dst, Rvalue::Use(operand)) = &stmt.kind {
if let (
Place::Local(dst_slot),
Operand::Copy(Place::Local(src_slot))
| Operand::Move(Place::Local(src_slot))
| Operand::MoveExplicit(Place::Local(src_slot)),
) = (dst, operand)
{
assign_pairs.push((*dst_slot, *src_slot));
}
}
}
}
// The second `let x = x` shadow must have dst != src (outer
// slot read into inner slot). Pre-fix this would be dst == src.
let shadow_assign = assign_pairs
.iter()
.find(|(dst, src)| dst != src)
.copied()
.expect("expected at least one Assign with dst != src");
let (shadow_dst, shadow_src) = shadow_assign;
assert_ne!(
shadow_dst, shadow_src,
"shadow `let x = x` must read OUTER x slot (not self-referential)"
);
}
#[test]
fn test_shadow_in_loop_threads_outer_value() {
// `let mut acc = 0; for item in [1,2,3] { acc = { let acc = acc;
// acc + item } }` — mirrors the Vec.reduce body shape after
// Phase C closure inlining. The block-result `Assign` must
// write the per-iteration sum back to OUTER acc.
//
// Empirical assertion: the for-loop body's last statement is
// `Assign(outer_acc, ...)` where the RHS Use references a slot
// populated by `acc + item` — NOT a self-Assign of inner_acc to
// itself.
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: true,
pattern: DestructurePattern::Identifier("acc".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(0), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test_shadow_in_loop", &[], &body, span());
// Sanity: the function lowered without panic.
assert!(mir.num_locals >= 1);
}
#[test]
fn test_let_x_x_does_not_self_reference_in_mir() {
// Negative test: lower `let x = 1; let x = x;` and confirm the
// resulting MIR's second Assign reads a DIFFERENT slot than it
// writes. Pre-fix produced `Assign(SlotId(n), Use(Copy(SlotId(n))))`.
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(42), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("x".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test_let_x_x", &[], &body, span());
// Walk all Assigns; ensure NONE have the pattern
// `Assign(Local(n), Use(Copy|Move(Local(n))))` (self-Use).
for block in mir.blocks.iter() {
for stmt in block.statements.iter() {
if let StatementKind::Assign(Place::Local(dst), Rvalue::Use(operand)) =
&stmt.kind
{
if let Operand::Copy(Place::Local(src))
| Operand::Move(Place::Local(src))
| Operand::MoveExplicit(Place::Local(src)) = operand
{
assert_ne!(
dst, src,
"MIR contains self-Use Assign(slot {:?}, Use(slot {:?})) — \
the shadow `let x = x` MUST NOT lower to a self-Assign \
(regression of v0.3 Vec.reduce fold-state JIT divergence)",
dst, src,
);
}
}
}
}
}
#[test]
fn test_non_shadow_let_preserves_outer_lookups() {
// Sanity: `let x = 1; let y = x;` works the same way after the
// fix as before — the second decl's slot is DIFFERENT from
// x's, and the RHS reads x's slot. This isn't a shadow, but
// exercises the deferred-bind path uniformly.
let body = vec![
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("x".to_string(), span()),
type_annotation: None,
value: Some(Expr::Literal(ast::Literal::Int(7), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
Statement::VariableDecl(
ast::VariableDecl {
kind: VarKind::Let,
is_mut: false,
pattern: DestructurePattern::Identifier("y".to_string(), span()),
type_annotation: None,
value: Some(Expr::Identifier("x".to_string(), span())),
ownership: OwnershipModifier::Inferred,
},
span(),
),
];
let mir = lower_function("test_non_shadow", &[], &body, span());
// y's slot must differ from x's slot, and the RHS of y's Assign
// reads x's slot.
let mut all_assigns = Vec::new();
for block in mir.blocks.iter() {
for stmt in block.statements.iter() {
if let StatementKind::Assign(Place::Local(dst), Rvalue::Use(operand)) =
&stmt.kind
{
if let Operand::Copy(Place::Local(src))
| Operand::Move(Place::Local(src))
| Operand::MoveExplicit(Place::Local(src)) = operand
{
all_assigns.push((*dst, *src));
}
}
}
}
// The slot-to-slot Use Assign (representing `let y = x`) must
// exist with dst != src.
let cross_slot_assign = all_assigns
.iter()
.find(|(dst, src)| dst != src)
.copied()
.expect("expected slot-to-slot Use Assign for `let y = x`");
let (y_dst, x_src) = cross_slot_assign;
assert_ne!(y_dst, x_src, "y must be a fresh slot");
}
}