Skip to main content

shape_vm/mir/lowering/
mod.rs

1//! MIR lowering: AST -> MIR.
2//!
3//! Converts Shape AST function bodies into MIR basic blocks.
4//! This is the bridge between parsing and borrow analysis.
5//!
6//! ## Module structure
7//!
8//! - [`mod.rs`](self) -- Public API (`lower_function`, `lower_function_detailed`,
9//!   `compute_mutability_errors`), `MirBuilder` struct and its state machine.
10//! - [`expr`] -- Expression lowering (`lower_expr_to_temp` and its many helpers).
11//! - [`stmt`] -- Statement lowering (variable decls, assignments, control flow,
12//!   pattern destructuring).
13//! - [`helpers`] -- Shared utilities: generic container store emission, operand
14//!   collection, place projection, type inference from expressions.
15
16mod expr;
17mod helpers;
18mod stmt;
19
20use super::types::*;
21use crate::mir::analysis::MutabilityError;
22use shape_ast::ast::{self, Span, Statement};
23use std::collections::{HashMap, HashSet};
24
25
26#[derive(Debug, Clone, Copy)]
27pub(super) struct MirLoopContext {
28    pub(super) break_block: BasicBlockId,
29    pub(super) continue_block: BasicBlockId,
30    pub(super) break_value_slot: Option<SlotId>,
31}
32
33#[derive(Debug, Clone)]
34struct TaskBoundaryCaptureScope {
35    outer_locals_cutoff: u16,
36    operands: Vec<Operand>,
37}
38
39#[derive(Debug, Clone)]
40struct MirLocalRecord {
41    name: String,
42    type_info: LocalTypeInfo,
43    binding_info: Option<LoweredBindingInfo>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct LoweredBindingInfo {
48    pub slot: SlotId,
49    pub name: String,
50    pub declaration_span: Span,
51    pub enforce_immutable_assignment: bool,
52    pub is_explicit_let: bool,
53    pub is_const: bool,
54    pub initialization_point: Option<Point>,
55}
56
57#[derive(Debug, Clone, Copy)]
58pub(super) struct BindingMetadata {
59    declaration_span: Span,
60    enforce_immutable_assignment: bool,
61    is_explicit_let: bool,
62    is_const: bool,
63}
64
65/// Builder for constructing a MIR function from AST.
66pub struct MirBuilder {
67    /// Name of the function being built.
68    name: String,
69    /// Completed basic blocks.
70    blocks: Vec<BasicBlock>,
71    /// Statements for the current (in-progress) basic block.
72    current_stmts: Vec<MirStatement>,
73    /// ID of the current basic block.
74    pub(super) current_block: BasicBlockId,
75    /// Whether the current block has already been terminated and stored.
76    current_block_finished: bool,
77    /// Next block ID to allocate.
78    next_block_id: u32,
79    /// Next local slot to allocate.
80    next_local: u16,
81    /// Dedicated return slot used by explicit `return` statements.
82    return_slot: SlotId,
83    /// Next program point.
84    next_point: u32,
85    /// Next loan ID.
86    next_loan: u32,
87    /// Local variable name -> slot mapping.
88    locals: Vec<MirLocalRecord>,
89    /// Active local name -> slot mapping for place resolution.
90    local_slots: HashMap<String, SlotId>,
91    /// Stable field indices for property-place lowering.
92    field_indices: HashMap<String, FieldIdx>,
93    /// Next field index to allocate.
94    next_field_idx: u16,
95    /// Parameter slots.
96    param_slots: Vec<SlotId>,
97    /// Per-parameter reference kind, aligned with `param_slots`.
98    param_reference_kinds: Vec<Option<BorrowKind>>,
99    /// Named-local shadowing stack for lexical scopes.
100    scope_bindings: Vec<Vec<(String, Option<SlotId>)>>,
101    /// Active loop control-flow targets.
102    loop_contexts: Vec<MirLoopContext>,
103    /// Active task-boundary capture scopes for async lowering.
104    task_boundary_capture_scopes: Vec<TaskBoundaryCaptureScope>,
105    /// Nesting depth of `async scope` blocks -- nonzero means structured concurrency.
106    pub(super) async_scope_depth: u32,
107    /// Exit block for the enclosing function.
108    exit_block: Option<BasicBlockId>,
109    /// Function span.
110    span: Span,
111    /// Spans where lowering had to fall back to placeholder/Nop handling.
112    /// Empty means clean lowering with no fallbacks.
113    fallback_spans: Vec<Span>,
114}
115
116#[derive(Debug)]
117pub struct MirLoweringResult {
118    pub mir: MirFunction,
119    pub had_fallbacks: bool,
120    /// Spans where lowering fell back to placeholder handling.
121    /// Used for span-granular error filtering in partial-authority mode.
122    pub fallback_spans: Vec<Span>,
123    pub binding_infos: Vec<LoweredBindingInfo>,
124    /// Reverse map from field index -> field name (inverted from `field_indices`).
125    pub field_names: HashMap<FieldIdx, String>,
126    /// All named locals (params + bindings), excluding `__mir_*` temporaries.
127    /// Used by callee summary filtering to detect local-name shadows.
128    pub all_local_names: HashSet<String>,
129}
130
131// ---------------------------------------------------------------------------
132// MirBuilder -- block and state machine management
133// ---------------------------------------------------------------------------
134
135impl MirBuilder {
136    pub fn new(name: String, span: Span) -> Self {
137        let return_slot = SlotId(0);
138        MirBuilder {
139            name,
140            blocks: Vec::new(),
141            current_stmts: Vec::new(),
142            current_block: BasicBlockId(0),
143            current_block_finished: false,
144            next_block_id: 1,
145            next_local: 1,
146            return_slot,
147            next_point: 0,
148            next_loan: 0,
149            locals: vec![MirLocalRecord {
150                name: "__mir_return".to_string(),
151                type_info: LocalTypeInfo::Unknown,
152                binding_info: None,
153            }],
154            local_slots: HashMap::new(),
155            field_indices: HashMap::new(),
156            next_field_idx: 0,
157            param_slots: Vec::new(),
158            param_reference_kinds: Vec::new(),
159            scope_bindings: vec![Vec::new()],
160            loop_contexts: Vec::new(),
161            task_boundary_capture_scopes: Vec::new(),
162            async_scope_depth: 0,
163            exit_block: None,
164            span,
165            fallback_spans: Vec::new(),
166        }
167    }
168
169    /// Allocate a new local variable slot.
170    pub fn alloc_local(&mut self, name: String, type_info: LocalTypeInfo) -> SlotId {
171        self.alloc_local_with_binding(name, type_info, None)
172    }
173
174    pub(super) fn alloc_local_binding(
175        &mut self,
176        name: String,
177        type_info: LocalTypeInfo,
178        binding_metadata: BindingMetadata,
179    ) -> SlotId {
180        self.alloc_local_with_binding(name, type_info, Some(binding_metadata))
181    }
182
183    fn alloc_local_with_binding(
184        &mut self,
185        name: String,
186        type_info: LocalTypeInfo,
187        binding_metadata: Option<BindingMetadata>,
188    ) -> SlotId {
189        let slot = SlotId(self.next_local);
190        self.next_local += 1;
191        let binding_info = binding_metadata.map(|binding_metadata| LoweredBindingInfo {
192            slot,
193            name: name.clone(),
194            declaration_span: binding_metadata.declaration_span,
195            enforce_immutable_assignment: binding_metadata.enforce_immutable_assignment,
196            is_explicit_let: binding_metadata.is_explicit_let,
197            is_const: binding_metadata.is_const,
198            initialization_point: None,
199        });
200        self.locals.push(MirLocalRecord {
201            name,
202            type_info,
203            binding_info,
204        });
205        if let Some(local) = self.locals.last()
206            && !local.name.starts_with("__mir_")
207        {
208            self.bind_named_local(local.name.clone(), slot);
209        }
210        slot
211    }
212
213    /// Allocate a temporary local slot that should not participate in name resolution.
214    pub fn alloc_temp(&mut self, type_info: LocalTypeInfo) -> SlotId {
215        let name = format!("__mir_tmp{}", self.next_local);
216        self.alloc_local(name, type_info)
217    }
218
219    /// Register a parameter slot.
220    fn add_param(
221        &mut self,
222        name: String,
223        type_info: LocalTypeInfo,
224        reference_kind: Option<BorrowKind>,
225        binding_metadata: Option<BindingMetadata>,
226    ) -> SlotId {
227        let slot = self.alloc_local_with_binding(name, type_info, binding_metadata);
228        self.param_slots.push(slot);
229        self.param_reference_kinds.push(reference_kind);
230        slot
231    }
232
233    /// Look up the current slot for a named local.
234    pub fn lookup_local(&self, name: &str) -> Option<SlotId> {
235        self.local_slots.get(name).copied()
236    }
237
238    pub fn visible_named_locals(&self) -> Vec<String> {
239        self.local_slots
240            .keys()
241            .filter(|name| !name.starts_with("__mir_"))
242            .cloned()
243            .collect()
244    }
245
246    /// Get or allocate a stable field index for a property name.
247    pub fn field_idx(&mut self, property: &str) -> FieldIdx {
248        if let Some(idx) = self.field_indices.get(property).copied() {
249            return idx;
250        }
251        let idx = FieldIdx(self.next_field_idx);
252        self.next_field_idx += 1;
253        self.field_indices.insert(property.to_string(), idx);
254        idx
255    }
256
257    pub fn return_slot(&self) -> SlotId {
258        self.return_slot
259    }
260
261    pub fn set_exit_block(&mut self, block: BasicBlockId) {
262        self.exit_block = Some(block);
263    }
264
265    pub fn exit_block(&self) -> BasicBlockId {
266        self.exit_block
267            .expect("MIR builder exit block should be initialized before lowering")
268    }
269
270    pub fn push_scope(&mut self) {
271        self.scope_bindings.push(Vec::new());
272    }
273
274    pub fn pop_scope(&mut self) {
275        if self.scope_bindings.len() <= 1 {
276            return;
277        }
278        if let Some(bindings) = self.scope_bindings.pop() {
279            for (name, previous_slot) in bindings.into_iter().rev() {
280                if let Some(slot) = previous_slot {
281                    self.local_slots.insert(name, slot);
282                } else {
283                    self.local_slots.remove(&name);
284                }
285            }
286        }
287    }
288
289    fn bind_named_local(&mut self, name: String, slot: SlotId) {
290        if let Some(scope) = self.scope_bindings.last_mut()
291            && !scope.iter().any(|(existing, _)| existing == &name)
292        {
293            scope.push((name.clone(), self.local_slots.get(&name).copied()));
294        }
295        self.local_slots.insert(name, slot);
296    }
297
298    pub fn mark_fallback(&mut self) {
299        // Legacy: called without a span. Use the current function span as fallback.
300        self.fallback_spans.push(self.span);
301    }
302
303    pub fn mark_fallback_at(&mut self, span: Span) {
304        self.fallback_spans.push(span);
305    }
306
307    pub fn had_fallbacks(&self) -> bool {
308        !self.fallback_spans.is_empty()
309    }
310
311    pub fn push_loop(
312        &mut self,
313        break_block: BasicBlockId,
314        continue_block: BasicBlockId,
315        break_value_slot: Option<SlotId>,
316    ) {
317        self.loop_contexts.push(MirLoopContext {
318            break_block,
319            continue_block,
320            break_value_slot,
321        });
322    }
323
324    pub fn pop_loop(&mut self) {
325        self.loop_contexts.pop();
326    }
327
328    pub(super) fn current_loop(&self) -> Option<MirLoopContext> {
329        self.loop_contexts.last().copied()
330    }
331
332    pub fn push_task_boundary_capture_scope(&mut self) {
333        self.task_boundary_capture_scopes
334            .push(TaskBoundaryCaptureScope {
335                outer_locals_cutoff: self.next_local,
336                operands: Vec::new(),
337            });
338    }
339
340    pub fn pop_task_boundary_capture_scope(&mut self) -> Vec<Operand> {
341        self.task_boundary_capture_scopes
342            .pop()
343            .map(|scope| scope.operands)
344            .unwrap_or_default()
345    }
346
347    pub fn record_task_boundary_operand(&mut self, operand: Operand) {
348        for scope in &mut self.task_boundary_capture_scopes {
349            if !helpers::operand_crosses_task_boundary(scope.outer_locals_cutoff, &operand) {
350                continue;
351            }
352            if !scope.operands.contains(&operand) {
353                scope.operands.push(operand.clone());
354            }
355        }
356    }
357
358    pub fn record_task_boundary_reference_capture(
359        &mut self,
360        reference_slot: SlotId,
361        borrowed_place: &Place,
362    ) {
363        let reference_operand = Operand::Copy(Place::Local(reference_slot));
364        for scope in &mut self.task_boundary_capture_scopes {
365            if borrowed_place.root_local().0 >= scope.outer_locals_cutoff {
366                continue;
367            }
368            if !scope.operands.contains(&reference_operand) {
369                scope.operands.push(reference_operand.clone());
370            }
371        }
372    }
373
374    /// Allocate a new program point.
375    pub fn next_point(&mut self) -> Point {
376        let p = Point(self.next_point);
377        self.next_point += 1;
378        p
379    }
380
381    /// Allocate a new loan ID.
382    pub fn next_loan(&mut self) -> LoanId {
383        let l = LoanId(self.next_loan);
384        self.next_loan += 1;
385        l
386    }
387
388    /// Create a new basic block and return its ID.
389    pub fn new_block(&mut self) -> BasicBlockId {
390        let id = BasicBlockId(self.next_block_id);
391        self.next_block_id += 1;
392        id
393    }
394
395    /// Push a statement into the current block.
396    pub fn push_stmt(&mut self, kind: StatementKind, span: Span) -> Point {
397        let point = self.next_point();
398        self.current_stmts.push(MirStatement { kind, span, point });
399        point
400    }
401
402    pub fn record_binding_initialization(&mut self, slot: SlotId, point: Point) {
403        if let Some(local) = self.locals.get_mut(slot.0 as usize)
404            && let Some(binding_info) = local.binding_info.as_mut()
405        {
406            binding_info.initialization_point = Some(point);
407        }
408    }
409
410    /// Finish the current block with a terminator and switch to a new block.
411    pub fn finish_block(&mut self, terminator_kind: TerminatorKind, span: Span) {
412        let block = BasicBlock {
413            id: self.current_block,
414            statements: std::mem::take(&mut self.current_stmts),
415            terminator: Terminator {
416                kind: terminator_kind,
417                span,
418            },
419        };
420        self.blocks.push(block);
421        self.current_block_finished = true;
422    }
423
424    /// Start building a new block (after finishing the previous one).
425    pub fn start_block(&mut self, id: BasicBlockId) {
426        self.current_block = id;
427        self.current_stmts.clear();
428        self.current_block_finished = false;
429    }
430
431    /// Emit a function call as a block terminator. Finishes current block
432    /// with TerminatorKind::Call and starts a continuation block.
433    pub fn emit_call(
434        &mut self,
435        func: Operand,
436        args: Vec<Operand>,
437        destination: Place,
438        span: Span,
439    ) {
440        let next_bb = self.new_block();
441        self.finish_block(
442            TerminatorKind::Call {
443                func,
444                args,
445                destination,
446                next: next_bb,
447            },
448            span,
449        );
450        self.start_block(next_bb);
451    }
452
453    /// Finalize and produce the MIR function.
454    pub fn build(self) -> MirLoweringResult {
455        let local_types = self
456            .locals
457            .iter()
458            .map(|local| local.type_info.clone())
459            .collect();
460        let binding_infos = self
461            .locals
462            .iter()
463            .filter_map(|local| local.binding_info.clone())
464            .collect();
465        let field_names: HashMap<FieldIdx, String> = self
466            .field_indices
467            .iter()
468            .map(|(name, &idx)| (idx, name.clone()))
469            .collect();
470        // Sort blocks by ID so that MirFunction::block(id) can index by id.0
471        let mut blocks = self.blocks;
472        blocks.sort_by_key(|b| b.id.0);
473
474        let had_fallbacks = !self.fallback_spans.is_empty();
475        let fallback_spans = self.fallback_spans;
476        let all_local_names: HashSet<String> = self
477            .locals
478            .iter()
479            .filter(|l| !l.name.starts_with("__mir_"))
480            .map(|l| l.name.clone())
481            .collect();
482
483        MirLoweringResult {
484            mir: MirFunction {
485                name: self.name,
486                blocks,
487                num_locals: self.next_local,
488                param_slots: self.param_slots,
489                param_reference_kinds: self.param_reference_kinds,
490                local_types,
491                span: self.span,
492            },
493            had_fallbacks,
494            fallback_spans,
495            binding_infos,
496            field_names,
497            all_local_names,
498        }
499    }
500}
501
502// ---------------------------------------------------------------------------
503// Public API
504// ---------------------------------------------------------------------------
505
506pub(super) fn immutable_binding_metadata(
507    declaration_span: Span,
508    is_explicit_let: bool,
509    is_const: bool,
510) -> BindingMetadata {
511    BindingMetadata {
512        declaration_span,
513        enforce_immutable_assignment: true,
514        is_explicit_let,
515        is_const,
516    }
517}
518
519/// Lower a function body (list of statements) into MIR.
520pub fn lower_function_detailed(
521    name: &str,
522    params: &[ast::FunctionParameter],
523    body: &[Statement],
524    span: Span,
525) -> MirLoweringResult {
526    let mut builder = MirBuilder::new(name.to_string(), span);
527
528    // Register parameters
529    for param in params {
530        let type_info = if param.is_reference {
531            LocalTypeInfo::NonCopy // references are always tracked
532        } else {
533            LocalTypeInfo::Unknown // will be resolved during analysis
534        };
535        let reference_kind = if param.is_mut_reference {
536            Some(BorrowKind::Exclusive)
537        } else if param.is_reference {
538            Some(BorrowKind::Shared)
539        } else {
540            None
541        };
542        let binding_metadata = if param.is_const {
543            Some(immutable_binding_metadata(param.span(), false, true))
544        } else if matches!(reference_kind, Some(BorrowKind::Shared)) {
545            Some(immutable_binding_metadata(param.span(), false, false))
546        } else {
547            None
548        };
549        if let Some(param_name) = param.simple_name() {
550            builder.add_param(
551                param_name.to_string(),
552                type_info,
553                reference_kind,
554                binding_metadata,
555            );
556        } else {
557            let slot = builder.add_param(
558                format!("__mir_param{}", builder.param_slots.len()),
559                type_info,
560                reference_kind,
561                None,
562            );
563            stmt::lower_destructure_bindings_from_place(
564                &mut builder,
565                &param.pattern,
566                &Place::Local(slot),
567                param.span(),
568                binding_metadata,
569            );
570        }
571    }
572
573    // Create the exit block
574    let exit_block = builder.new_block();
575    builder.set_exit_block(exit_block);
576
577    // Lower body statements
578    stmt::lower_statements(&mut builder, body, exit_block);
579
580    // If current block hasn't been finished (no explicit return), emit goto exit
581    if !builder.current_block_finished {
582        builder.finish_block(TerminatorKind::Goto(exit_block), span);
583    }
584
585    // Create exit block with Return terminator
586    builder.start_block(exit_block);
587    builder.finish_block(TerminatorKind::Return, span);
588
589    builder.build()
590}
591
592/// Lower a function body (list of statements) into MIR.
593pub fn lower_function(
594    name: &str,
595    params: &[ast::FunctionParameter],
596    body: &[Statement],
597    span: Span,
598) -> MirFunction {
599    lower_function_detailed(name, params, body, span).mir
600}
601
602pub fn compute_mutability_errors(lowering: &MirLoweringResult) -> Vec<MutabilityError> {
603    let tracked_bindings: HashMap<SlotId, &LoweredBindingInfo> = lowering
604        .binding_infos
605        .iter()
606        .filter(|binding| binding.enforce_immutable_assignment)
607        .map(|binding| (binding.slot, binding))
608        .collect();
609    let mut errors = Vec::new();
610
611    for block in &lowering.mir.blocks {
612        for stmt in &block.statements {
613            let StatementKind::Assign(place, _) = &stmt.kind else {
614                continue;
615            };
616            let root = place.root_local();
617            let Some(binding) = tracked_bindings.get(&root) else {
618                continue;
619            };
620            let is_declaration_init = matches!(place, Place::Local(slot) if *slot == root)
621                && binding.initialization_point == Some(stmt.point);
622            if is_declaration_init {
623                continue;
624            }
625            errors.push(MutabilityError {
626                span: stmt.span,
627                variable_name: binding.name.clone(),
628                declaration_span: binding.declaration_span,
629                is_explicit_let: binding.is_explicit_let,
630                is_const: binding.is_const,
631            });
632        }
633    }
634
635    errors
636}
637
638// ---------------------------------------------------------------------------
639// Tests
640// ---------------------------------------------------------------------------
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645    use crate::mir::analysis::BorrowErrorKind;
646    use crate::mir::cfg::ControlFlowGraph;
647    use crate::mir::liveness;
648    use crate::mir::solver;
649    use shape_ast::ast::{self, DestructurePattern, Expr, OwnershipModifier, VarKind};
650
651    fn span() -> Span {
652        Span { start: 0, end: 1 }
653    }
654
655    fn lower_parsed_function(code: &str) -> MirLoweringResult {
656        let program = shape_ast::parser::parse_program(code).expect("parse failed");
657        let func = match &program.items[0] {
658            ast::Item::Function(func, _) => func,
659            _ => panic!("expected function item"),
660        };
661        lower_function_detailed(&func.name, &func.params, &func.body, func.name_span)
662    }
663
664    #[test]
665    fn test_lower_empty_function() {
666        let mir = lower_function("empty", &[], &[], span());
667        assert_eq!(mir.name, "empty");
668        assert!(mir.blocks.len() >= 2); // entry + exit
669        assert_eq!(mir.num_locals, 1);
670    }
671
672    #[test]
673    fn test_lower_simple_var_decl() {
674        let body = vec![Statement::VariableDecl(
675            ast::VariableDecl {
676                kind: VarKind::Let,
677                is_mut: false,
678                pattern: DestructurePattern::Identifier("x".to_string(), span()),
679                type_annotation: None,
680                value: Some(Expr::Literal(ast::Literal::Int(42), span())),
681                ownership: OwnershipModifier::Inferred,
682            },
683            span(),
684        )];
685        let mir = lower_function("test", &[], &body, span());
686        assert!(mir.num_locals >= 1); // at least x + temp
687        // Should have at least 2 blocks (entry + exit)
688        assert!(mir.blocks.len() >= 2);
689    }
690
691    #[test]
692    fn test_compute_mutability_errors_ignores_binding_initializer() {
693        let lowering = lower_parsed_function(
694            r#"
695                function keep() {
696                    let x = 1
697                    x
698                }
699            "#,
700        );
701        let errors = compute_mutability_errors(&lowering);
702        assert!(
703            errors.is_empty(),
704            "declaration initializer should not be reported as a mutability error: {:?}",
705            errors
706        );
707    }
708
709    #[test]
710    fn test_compute_mutability_errors_flags_immutable_let_reassignment() {
711        let lowering = lower_parsed_function(
712            r#"
713                function mutate() {
714                    let x = 1
715                    x = 2
716                    x
717                }
718            "#,
719        );
720        let errors = compute_mutability_errors(&lowering);
721        assert_eq!(
722            errors.len(),
723            1,
724            "expected one mutability error, got {errors:?}"
725        );
726        assert_eq!(errors[0].variable_name, "x");
727        assert!(errors[0].is_explicit_let);
728    }
729
730    #[test]
731    fn test_compute_mutability_errors_flags_const_reassignment() {
732        let lowering = lower_parsed_function(
733            r#"
734                function mutate() {
735                    const x = 1
736                    x = 2
737                    x
738                }
739            "#,
740        );
741        let errors = compute_mutability_errors(&lowering);
742        assert_eq!(
743            errors.len(),
744            1,
745            "expected one mutability error, got {errors:?}"
746        );
747        assert_eq!(errors[0].variable_name, "x");
748        assert!(errors[0].is_const);
749    }
750
751    #[test]
752    fn test_compute_mutability_errors_flags_shared_ref_param_write() {
753        let lowering = lower_parsed_function(
754            r#"
755                function mutate(&x) {
756                    x = 2
757                    x
758                }
759            "#,
760        );
761        let errors = compute_mutability_errors(&lowering);
762        assert_eq!(
763            errors.len(),
764            1,
765            "expected one mutability error, got {errors:?}"
766        );
767        assert_eq!(errors[0].variable_name, "x");
768        assert!(!errors[0].is_explicit_let);
769    }
770
771    #[test]
772    fn test_compute_mutability_errors_flags_const_param_write() {
773        let lowering = lower_parsed_function(
774            r#"
775                function mutate(const x) {
776                    x = 2
777                    x
778                }
779            "#,
780        );
781        let errors = compute_mutability_errors(&lowering);
782        assert_eq!(
783            errors.len(),
784            1,
785            "expected one mutability error, got {errors:?}"
786        );
787        assert_eq!(errors[0].variable_name, "x");
788        assert!(errors[0].is_const);
789    }
790
791    #[test]
792    fn test_lower_with_liveness() {
793        // let x = 1; let y = x; (x live after first stmt, dead after second)
794        let body = vec![
795            Statement::VariableDecl(
796                ast::VariableDecl {
797                    kind: VarKind::Let,
798                    is_mut: false,
799                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
800                    type_annotation: None,
801                    value: Some(Expr::Literal(
802                        ast::Literal::String("hi".to_string()),
803                        span(),
804                    )),
805                    ownership: OwnershipModifier::Inferred,
806                },
807                span(),
808            ),
809            Statement::VariableDecl(
810                ast::VariableDecl {
811                    kind: VarKind::Let,
812                    is_mut: false,
813                    pattern: DestructurePattern::Identifier("y".to_string(), span()),
814                    type_annotation: None,
815                    value: Some(Expr::Identifier("x".to_string(), span())),
816                    ownership: OwnershipModifier::Inferred,
817                },
818                span(),
819            ),
820            Statement::VariableDecl(
821                ast::VariableDecl {
822                    kind: VarKind::Let,
823                    is_mut: false,
824                    pattern: DestructurePattern::Identifier("kept".to_string(), span()),
825                    type_annotation: None,
826                    value: Some(Expr::Identifier("shared".to_string(), span())),
827                    ownership: OwnershipModifier::Inferred,
828                },
829                span(),
830            ),
831        ];
832        let mir = lower_function("test", &[], &body, span());
833        let cfg = ControlFlowGraph::build(&mir);
834        let _liveness = liveness::compute_liveness(&mir, &cfg);
835        // The MIR lowers and liveness computes without panic
836    }
837
838    #[test]
839    fn test_lower_reference_to_identifier_borrows_original_local() {
840        let body = vec![
841            Statement::VariableDecl(
842                ast::VariableDecl {
843                    kind: VarKind::Let,
844                    is_mut: false,
845                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
846                    type_annotation: None,
847                    value: Some(Expr::Literal(
848                        ast::Literal::String("hi".to_string()),
849                        span(),
850                    )),
851                    ownership: OwnershipModifier::Inferred,
852                },
853                span(),
854            ),
855            Statement::VariableDecl(
856                ast::VariableDecl {
857                    kind: VarKind::Let,
858                    is_mut: false,
859                    pattern: DestructurePattern::Identifier("r".to_string(), span()),
860                    type_annotation: None,
861                    value: Some(Expr::Reference {
862                        expr: Box::new(Expr::Identifier("x".to_string(), span())),
863                        is_mutable: false,
864                        span: span(),
865                    }),
866                    ownership: OwnershipModifier::Inferred,
867                },
868                span(),
869            ),
870        ];
871        let mir = lower_function("test", &[], &body, span());
872        let borrow_place = mir
873            .blocks
874            .iter()
875            .flat_map(|block| block.statements.iter())
876            .find_map(|stmt| match &stmt.kind {
877                StatementKind::Assign(_, Rvalue::Borrow(_, place)) => Some(place.clone()),
878                _ => None,
879            })
880            .expect("expected borrow statement");
881        assert_eq!(borrow_place, Place::Local(SlotId(1)));
882    }
883
884    #[test]
885    fn test_lowered_local_borrow_conflict_is_visible_to_solver() {
886        let body = vec![
887            Statement::VariableDecl(
888                ast::VariableDecl {
889                    kind: VarKind::Let,
890                    is_mut: true,
891                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
892                    type_annotation: None,
893                    value: Some(Expr::Literal(ast::Literal::Int(1), span())),
894                    ownership: OwnershipModifier::Inferred,
895                },
896                span(),
897            ),
898            Statement::VariableDecl(
899                ast::VariableDecl {
900                    kind: VarKind::Let,
901                    is_mut: false,
902                    pattern: DestructurePattern::Identifier("shared".to_string(), span()),
903                    type_annotation: None,
904                    value: Some(Expr::Reference {
905                        expr: Box::new(Expr::Identifier("x".to_string(), span())),
906                        is_mutable: false,
907                        span: span(),
908                    }),
909                    ownership: OwnershipModifier::Inferred,
910                },
911                span(),
912            ),
913            Statement::VariableDecl(
914                ast::VariableDecl {
915                    kind: VarKind::Let,
916                    is_mut: false,
917                    pattern: DestructurePattern::Identifier("exclusive".to_string(), span()),
918                    type_annotation: None,
919                    value: Some(Expr::Reference {
920                        expr: Box::new(Expr::Identifier("x".to_string(), span())),
921                        is_mutable: true,
922                        span: span(),
923                    }),
924                    ownership: OwnershipModifier::Inferred,
925                },
926                span(),
927            ),
928            Statement::Return(Some(Expr::Identifier("shared".to_string(), span())), span()),
929        ];
930        let mir = lower_function("test", &[], &body, span());
931        let analysis = solver::analyze(&mir, &Default::default());
932        assert!(
933            analysis
934                .errors
935                .iter()
936                .any(|error| error.kind == BorrowErrorKind::ConflictSharedExclusive),
937            "expected shared/exclusive conflict, got {:?}",
938            analysis.errors
939        );
940    }
941
942    #[test]
943    fn test_lowered_property_borrows_preserve_disjoint_places() {
944        let body = vec![
945            Statement::VariableDecl(
946                ast::VariableDecl {
947                    kind: VarKind::Let,
948                    is_mut: true,
949                    pattern: DestructurePattern::Identifier("pair".to_string(), span()),
950                    type_annotation: None,
951                    value: Some(Expr::Literal(ast::Literal::Int(0), span())),
952                    ownership: OwnershipModifier::Inferred,
953                },
954                span(),
955            ),
956            Statement::VariableDecl(
957                ast::VariableDecl {
958                    kind: VarKind::Let,
959                    is_mut: false,
960                    pattern: DestructurePattern::Identifier("left".to_string(), span()),
961                    type_annotation: None,
962                    value: Some(Expr::Reference {
963                        expr: Box::new(Expr::PropertyAccess {
964                            object: Box::new(Expr::Identifier("pair".to_string(), span())),
965                            property: "left".to_string(),
966                            optional: false,
967                            span: span(),
968                        }),
969                        is_mutable: true,
970                        span: span(),
971                    }),
972                    ownership: OwnershipModifier::Inferred,
973                },
974                span(),
975            ),
976            Statement::VariableDecl(
977                ast::VariableDecl {
978                    kind: VarKind::Let,
979                    is_mut: false,
980                    pattern: DestructurePattern::Identifier("right".to_string(), span()),
981                    type_annotation: None,
982                    value: Some(Expr::Reference {
983                        expr: Box::new(Expr::PropertyAccess {
984                            object: Box::new(Expr::Identifier("pair".to_string(), span())),
985                            property: "right".to_string(),
986                            optional: false,
987                            span: span(),
988                        }),
989                        is_mutable: true,
990                        span: span(),
991                    }),
992                    ownership: OwnershipModifier::Inferred,
993                },
994                span(),
995            ),
996            Statement::VariableDecl(
997                ast::VariableDecl {
998                    kind: VarKind::Let,
999                    is_mut: false,
1000                    pattern: DestructurePattern::Identifier("kept".to_string(), span()),
1001                    type_annotation: None,
1002                    value: Some(Expr::Identifier("shared".to_string(), span())),
1003                    ownership: OwnershipModifier::Inferred,
1004                },
1005                span(),
1006            ),
1007        ];
1008        let mir = lower_function("test", &[], &body, span());
1009        let analysis = solver::analyze(&mir, &Default::default());
1010        assert!(
1011            analysis.errors.is_empty(),
1012            "disjoint field borrows should not conflict, got {:?}",
1013            analysis.errors
1014        );
1015    }
1016
1017    #[test]
1018    fn test_lowered_write_while_borrowed_is_visible_to_solver() {
1019        let body = vec![
1020            Statement::VariableDecl(
1021                ast::VariableDecl {
1022                    kind: VarKind::Let,
1023                    is_mut: true,
1024                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
1025                    type_annotation: None,
1026                    value: Some(Expr::Literal(ast::Literal::Int(1), span())),
1027                    ownership: OwnershipModifier::Inferred,
1028                },
1029                span(),
1030            ),
1031            Statement::VariableDecl(
1032                ast::VariableDecl {
1033                    kind: VarKind::Let,
1034                    is_mut: false,
1035                    pattern: DestructurePattern::Identifier("shared".to_string(), span()),
1036                    type_annotation: None,
1037                    value: Some(Expr::Reference {
1038                        expr: Box::new(Expr::Identifier("x".to_string(), span())),
1039                        is_mutable: false,
1040                        span: span(),
1041                    }),
1042                    ownership: OwnershipModifier::Inferred,
1043                },
1044                span(),
1045            ),
1046            Statement::Assignment(
1047                ast::Assignment {
1048                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
1049                    value: Expr::Literal(ast::Literal::Int(2), span()),
1050                },
1051                span(),
1052            ),
1053            Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
1054        ];
1055        let mir = lower_function("test", &[], &body, span());
1056        let analysis = solver::analyze(&mir, &Default::default());
1057        assert!(
1058            analysis
1059                .errors
1060                .iter()
1061                .any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed),
1062            "expected write-while-borrowed error, got {:?}",
1063            analysis.errors
1064        );
1065    }
1066
1067    #[test]
1068    fn test_lowered_read_while_exclusive_borrow_is_visible_to_solver() {
1069        let body = vec![
1070            Statement::VariableDecl(
1071                ast::VariableDecl {
1072                    kind: VarKind::Let,
1073                    is_mut: true,
1074                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
1075                    type_annotation: None,
1076                    value: Some(Expr::Literal(ast::Literal::Int(1), span())),
1077                    ownership: OwnershipModifier::Inferred,
1078                },
1079                span(),
1080            ),
1081            Statement::VariableDecl(
1082                ast::VariableDecl {
1083                    kind: VarKind::Let,
1084                    is_mut: false,
1085                    pattern: DestructurePattern::Identifier("exclusive".to_string(), span()),
1086                    type_annotation: None,
1087                    value: Some(Expr::Reference {
1088                        expr: Box::new(Expr::Identifier("x".to_string(), span())),
1089                        is_mutable: true,
1090                        span: span(),
1091                    }),
1092                    ownership: OwnershipModifier::Inferred,
1093                },
1094                span(),
1095            ),
1096            Statement::VariableDecl(
1097                ast::VariableDecl {
1098                    kind: VarKind::Let,
1099                    is_mut: false,
1100                    pattern: DestructurePattern::Identifier("copy".to_string(), span()),
1101                    type_annotation: None,
1102                    value: Some(Expr::Identifier("x".to_string(), span())),
1103                    ownership: OwnershipModifier::Inferred,
1104                },
1105                span(),
1106            ),
1107            Statement::Expression(Expr::Identifier("exclusive".to_string(), span()), span()),
1108        ];
1109        let mir = lower_function("test", &[], &body, span());
1110        let analysis = solver::analyze(&mir, &Default::default());
1111        assert!(
1112            analysis
1113                .errors
1114                .iter()
1115                .any(|error| error.kind == BorrowErrorKind::ReadWhileExclusivelyBorrowed),
1116            "expected read-while-exclusive error, got {:?}",
1117            analysis.errors
1118        );
1119    }
1120
1121    #[test]
1122    fn test_lowered_returned_ref_alias_is_visible_to_solver() {
1123        let body = vec![
1124            Statement::VariableDecl(
1125                ast::VariableDecl {
1126                    kind: VarKind::Let,
1127                    is_mut: false,
1128                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
1129                    type_annotation: None,
1130                    value: Some(Expr::Literal(ast::Literal::Int(1), span())),
1131                    ownership: OwnershipModifier::Inferred,
1132                },
1133                span(),
1134            ),
1135            Statement::VariableDecl(
1136                ast::VariableDecl {
1137                    kind: VarKind::Let,
1138                    is_mut: false,
1139                    pattern: DestructurePattern::Identifier("r".to_string(), span()),
1140                    type_annotation: None,
1141                    value: Some(Expr::Reference {
1142                        expr: Box::new(Expr::Identifier("x".to_string(), span())),
1143                        is_mutable: false,
1144                        span: span(),
1145                    }),
1146                    ownership: OwnershipModifier::Inferred,
1147                },
1148                span(),
1149            ),
1150            Statement::VariableDecl(
1151                ast::VariableDecl {
1152                    kind: VarKind::Let,
1153                    is_mut: false,
1154                    pattern: DestructurePattern::Identifier("alias".to_string(), span()),
1155                    type_annotation: None,
1156                    value: Some(Expr::Identifier("r".to_string(), span())),
1157                    ownership: OwnershipModifier::Inferred,
1158                },
1159                span(),
1160            ),
1161            Statement::Return(Some(Expr::Identifier("alias".to_string(), span())), span()),
1162        ];
1163        let mir = lower_function("test", &[], &body, span());
1164        let analysis = solver::analyze(&mir, &Default::default());
1165        assert!(
1166            analysis
1167                .errors
1168                .iter()
1169                .any(|error| error.kind == BorrowErrorKind::ReferenceEscape),
1170            "expected reference-escape error, got {:?}",
1171            analysis.errors
1172        );
1173    }
1174
1175    #[test]
1176    fn test_lowered_array_direct_ref_escape_is_visible_to_solver() {
1177        let lowering = lower_parsed_function(
1178            r#"
1179                function test() {
1180                    let x = 1
1181                    let arr = [&x]
1182                }
1183            "#,
1184        );
1185        assert!(!lowering.had_fallbacks);
1186        let analysis = solver::analyze(&lowering.mir, &Default::default());
1187        assert!(analysis.errors.is_empty());
1188    }
1189
1190    #[test]
1191    fn test_lowered_array_indirect_ref_escape_is_visible_to_solver() {
1192        let lowering = lower_parsed_function(
1193            r#"
1194                function test() {
1195                    let x = 1
1196                    let r = &x
1197                    let arr = [r]
1198                }
1199            "#,
1200        );
1201        assert!(!lowering.had_fallbacks);
1202        let analysis = solver::analyze(&lowering.mir, &Default::default());
1203        assert!(analysis.errors.is_empty());
1204    }
1205
1206    #[test]
1207    fn test_lowered_object_direct_ref_escape_is_visible_to_solver() {
1208        let lowering = lower_parsed_function(
1209            r#"
1210                function test() {
1211                    let x = 1
1212                    let obj = { value: &x }
1213                }
1214            "#,
1215        );
1216        assert!(!lowering.had_fallbacks);
1217        let analysis = solver::analyze(&lowering.mir, &Default::default());
1218        assert!(analysis.errors.is_empty());
1219    }
1220
1221    #[test]
1222    fn test_lowered_object_indirect_ref_escape_is_visible_to_solver() {
1223        let lowering = lower_parsed_function(
1224            r#"
1225                function test() {
1226                    let x = 1
1227                    let r = &x
1228                    let obj = { value: r }
1229                }
1230            "#,
1231        );
1232        assert!(!lowering.had_fallbacks);
1233        let analysis = solver::analyze(&lowering.mir, &Default::default());
1234        assert!(analysis.errors.is_empty());
1235    }
1236
1237    #[test]
1238    fn test_lowered_struct_direct_ref_escape_is_visible_to_solver() {
1239        let lowering = lower_parsed_function(
1240            r#"
1241                function test() {
1242                    let x = 1
1243                    let point = Point { value: &x }
1244                }
1245            "#,
1246        );
1247        assert!(!lowering.had_fallbacks);
1248        let analysis = solver::analyze(&lowering.mir, &Default::default());
1249        assert!(analysis.errors.is_empty());
1250    }
1251
1252    #[test]
1253    fn test_lowered_struct_indirect_ref_escape_is_visible_to_solver() {
1254        let lowering = lower_parsed_function(
1255            r#"
1256                function test() {
1257                    let x = 1
1258                    let r = &x
1259                    let point = Point { value: r }
1260                }
1261            "#,
1262        );
1263        assert!(!lowering.had_fallbacks);
1264        let analysis = solver::analyze(&lowering.mir, &Default::default());
1265        assert!(analysis.errors.is_empty());
1266    }
1267
1268    #[test]
1269    fn test_lowered_enum_tuple_direct_ref_escape_is_visible_to_solver() {
1270        let lowering = lower_parsed_function(
1271            r#"
1272                function test() {
1273                    let x = 1
1274                    let value = Maybe::Some(&x)
1275                }
1276            "#,
1277        );
1278        assert!(!lowering.had_fallbacks);
1279        let analysis = solver::analyze(&lowering.mir, &Default::default());
1280        assert!(analysis.errors.is_empty());
1281    }
1282
1283    #[test]
1284    fn test_lowered_enum_tuple_indirect_ref_escape_is_visible_to_solver() {
1285        let lowering = lower_parsed_function(
1286            r#"
1287                function test() {
1288                    let x = 1
1289                    let r = &x
1290                    let value = Maybe::Some(r)
1291                }
1292            "#,
1293        );
1294        assert!(!lowering.had_fallbacks);
1295        let analysis = solver::analyze(&lowering.mir, &Default::default());
1296        assert!(analysis.errors.is_empty());
1297    }
1298
1299    #[test]
1300    fn test_lowered_enum_struct_direct_ref_escape_is_visible_to_solver() {
1301        let lowering = lower_parsed_function(
1302            r#"
1303                function test() {
1304                    let x = 1
1305                    let value = Maybe::Err { code: &x }
1306                }
1307            "#,
1308        );
1309        assert!(!lowering.had_fallbacks);
1310        let analysis = solver::analyze(&lowering.mir, &Default::default());
1311        assert!(analysis.errors.is_empty());
1312    }
1313
1314    #[test]
1315    fn test_lowered_enum_struct_indirect_ref_escape_is_visible_to_solver() {
1316        let lowering = lower_parsed_function(
1317            r#"
1318                function test() {
1319                    let x = 1
1320                    let r = &x
1321                    let value = Maybe::Err { code: r }
1322                }
1323            "#,
1324        );
1325        assert!(!lowering.had_fallbacks);
1326        let analysis = solver::analyze(&lowering.mir, &Default::default());
1327        assert!(analysis.errors.is_empty());
1328    }
1329
1330    #[test]
1331    fn test_lowered_use_after_explicit_move_is_visible_to_solver() {
1332        let body = vec![
1333            Statement::VariableDecl(
1334                ast::VariableDecl {
1335                    kind: VarKind::Let,
1336                    is_mut: false,
1337                    pattern: DestructurePattern::Identifier("x".to_string(), span()),
1338                    type_annotation: None,
1339                    value: Some(Expr::Literal(
1340                        ast::Literal::String("hi".to_string()),
1341                        span(),
1342                    )),
1343                    ownership: OwnershipModifier::Inferred,
1344                },
1345                span(),
1346            ),
1347            Statement::VariableDecl(
1348                ast::VariableDecl {
1349                    kind: VarKind::Let,
1350                    is_mut: false,
1351                    pattern: DestructurePattern::Identifier("y".to_string(), span()),
1352                    type_annotation: None,
1353                    value: Some(Expr::Identifier("x".to_string(), span())),
1354                    ownership: OwnershipModifier::Move,
1355                },
1356                span(),
1357            ),
1358            Statement::VariableDecl(
1359                ast::VariableDecl {
1360                    kind: VarKind::Let,
1361                    is_mut: false,
1362                    pattern: DestructurePattern::Identifier("z".to_string(), span()),
1363                    type_annotation: None,
1364                    value: Some(Expr::Identifier("x".to_string(), span())),
1365                    ownership: OwnershipModifier::Inferred,
1366                },
1367                span(),
1368            ),
1369        ];
1370        let mir = lower_function("test", &[], &body, span());
1371        let analysis = solver::analyze(&mir, &Default::default());
1372        assert!(
1373            analysis
1374                .errors
1375                .iter()
1376                .any(|error| error.kind == BorrowErrorKind::UseAfterMove),
1377            "expected use-after-move error, got {:?}",
1378            analysis.errors
1379        );
1380    }
1381
1382    #[test]
1383    fn test_lowered_while_expr_write_while_borrowed_is_visible_to_solver() {
1384        let lowering = lower_parsed_function(
1385            r#"
1386                function test() {
1387                    let mut x = 1
1388                    let y = while true {
1389                        let shared = &x
1390                        x = 2
1391                        shared
1392                        0
1393                    }
1394                }
1395            "#,
1396        );
1397        assert!(!lowering.had_fallbacks);
1398        let analysis = solver::analyze(&lowering.mir, &Default::default());
1399        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1400    }
1401
1402    #[test]
1403    fn test_lowered_for_expr_write_while_borrowed_is_visible_to_solver() {
1404        let lowering = lower_parsed_function(
1405            r#"
1406                function test(items) {
1407                    let mut x = 1
1408                    let y = for item in items {
1409                        let shared = &x
1410                        x = 2
1411                        shared
1412                        0
1413                    }
1414                }
1415            "#,
1416        );
1417        assert!(!lowering.had_fallbacks);
1418        let analysis = solver::analyze(&lowering.mir, &Default::default());
1419        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1420    }
1421
1422    #[test]
1423    fn test_lowered_loop_expr_break_value_write_while_borrowed_is_visible_to_solver() {
1424        let lowering = lower_parsed_function(
1425            r#"
1426                function test() {
1427                    let mut x = 1
1428                    let y = loop {
1429                        let shared = &x
1430                        x = 2
1431                        shared
1432                        break 0
1433                    }
1434                }
1435            "#,
1436        );
1437        assert!(!lowering.had_fallbacks);
1438        let analysis = solver::analyze(&lowering.mir, &Default::default());
1439        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1440    }
1441
1442    #[test]
1443    fn test_lowered_continue_expression_in_while_body_stays_supported() {
1444        let lowering = lower_parsed_function(
1445            r#"
1446                function test(flag) {
1447                    let mut x = 1
1448                    let y = while flag {
1449                        if flag { continue } else { x }
1450                    }
1451                }
1452            "#,
1453        );
1454        assert!(!lowering.had_fallbacks);
1455        let analysis = solver::analyze(&lowering.mir, &Default::default());
1456        assert!(analysis.errors.is_empty());
1457    }
1458
1459    #[test]
1460    fn test_lowered_match_expression_write_while_borrowed_is_visible_to_solver() {
1461        let lowering = lower_parsed_function(
1462            r#"
1463                function test(flag) {
1464                    let mut x = 1
1465                    let y = match flag {
1466                        true => {
1467                            let shared = &x
1468                            x = 2
1469                            shared
1470                            0
1471                        }
1472                        _ => 0
1473                    }
1474                }
1475            "#,
1476        );
1477        assert!(!lowering.had_fallbacks);
1478        let analysis = solver::analyze(&lowering.mir, &Default::default());
1479        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1480    }
1481
1482    #[test]
1483    fn test_lowered_match_expression_identifier_guard_stays_supported() {
1484        let lowering = lower_parsed_function(
1485            r#"
1486                function test(v) {
1487                    let y = match v {
1488                        x where x > 0 => x
1489                        _ => 0
1490                    }
1491                }
1492            "#,
1493        );
1494        assert!(!lowering.had_fallbacks);
1495        let analysis = solver::analyze(&lowering.mir, &Default::default());
1496        assert!(analysis.errors.is_empty());
1497    }
1498
1499    #[test]
1500    fn test_lowered_match_expression_array_pattern_write_while_borrowed_is_visible_to_solver() {
1501        let lowering = lower_parsed_function(
1502            r#"
1503                function test(pair) {
1504                    let mut x = 1
1505                    let y = match pair {
1506                        [left, right] => {
1507                            let shared = &x
1508                            x = 2
1509                            shared
1510                            0
1511                        }
1512                        _ => 0
1513                    }
1514                }
1515            "#,
1516        );
1517        assert!(!lowering.had_fallbacks);
1518        let analysis = solver::analyze(&lowering.mir, &Default::default());
1519        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1520    }
1521
1522    #[test]
1523    fn test_lowered_match_expression_object_pattern_write_while_borrowed_is_visible_to_solver() {
1524        let lowering = lower_parsed_function(
1525            r#"
1526                function test(obj) {
1527                    let mut x = 1
1528                    let y = match obj {
1529                        { left: l, right: r } => {
1530                            let shared = &x
1531                            x = 2
1532                            shared
1533                            0
1534                        }
1535                        _ => 0
1536                    }
1537                }
1538            "#,
1539        );
1540        assert!(!lowering.had_fallbacks);
1541        let analysis = solver::analyze(&lowering.mir, &Default::default());
1542        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1543    }
1544
1545    #[test]
1546    fn test_lowered_match_expression_constructor_pattern_write_while_borrowed_is_visible_to_solver()
1547    {
1548        let lowering = lower_parsed_function(
1549            r#"
1550                function test(opt) {
1551                    let mut x = 1
1552                    let y = match opt {
1553                        Some(v) => {
1554                            let shared = &x
1555                            x = 2
1556                            shared
1557                            0
1558                        }
1559                        None => 0
1560                    }
1561                }
1562            "#,
1563        );
1564        assert!(!lowering.had_fallbacks);
1565        let analysis = solver::analyze(&lowering.mir, &Default::default());
1566        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1567    }
1568
1569    #[test]
1570    fn test_lowered_destructure_var_decl_write_while_borrowed_is_visible_to_solver() {
1571        let lowering = lower_parsed_function(
1572            r#"
1573                function test(pair) {
1574                    var [left, right] = pair
1575                    let shared = &left
1576                    left = 2
1577                    shared
1578                }
1579            "#,
1580        );
1581        assert!(!lowering.had_fallbacks);
1582        let analysis = solver::analyze(&lowering.mir, &Default::default());
1583        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1584    }
1585
1586    #[test]
1587    fn test_lowered_destructure_param_write_while_borrowed_is_visible_to_solver() {
1588        let lowering = lower_parsed_function(
1589            r#"
1590                function test([left, right]) {
1591                    let mut left_copy = left
1592                    let shared = &left_copy
1593                    left_copy = 2
1594                    shared
1595                }
1596            "#,
1597        );
1598        assert!(!lowering.had_fallbacks);
1599        let analysis = solver::analyze(&lowering.mir, &Default::default());
1600        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1601    }
1602
1603    #[test]
1604    fn test_lowered_destructure_assignment_stays_supported() {
1605        let pair_param = ast::FunctionParameter {
1606            pattern: DestructurePattern::Identifier("pair".to_string(), span()),
1607            is_const: false,
1608            is_reference: false,
1609            is_mut_reference: false,
1610            is_out: false,
1611            type_annotation: None,
1612            default_value: None,
1613        };
1614        let body = vec![
1615            Statement::VariableDecl(
1616                ast::VariableDecl {
1617                    kind: VarKind::Let,
1618                    is_mut: true,
1619                    pattern: DestructurePattern::Identifier("left".to_string(), span()),
1620                    type_annotation: None,
1621                    value: Some(Expr::Literal(ast::Literal::Int(1), span())),
1622                    ownership: OwnershipModifier::Inferred,
1623                },
1624                span(),
1625            ),
1626            Statement::VariableDecl(
1627                ast::VariableDecl {
1628                    kind: VarKind::Let,
1629                    is_mut: true,
1630                    pattern: DestructurePattern::Identifier("right".to_string(), span()),
1631                    type_annotation: None,
1632                    value: Some(Expr::Literal(ast::Literal::Int(2), span())),
1633                    ownership: OwnershipModifier::Inferred,
1634                },
1635                span(),
1636            ),
1637            Statement::Assignment(
1638                ast::Assignment {
1639                    pattern: DestructurePattern::Array(vec![
1640                        DestructurePattern::Identifier("left".to_string(), span()),
1641                        DestructurePattern::Identifier("right".to_string(), span()),
1642                    ]),
1643                    value: Expr::Identifier("pair".to_string(), span()),
1644                },
1645                span(),
1646            ),
1647            Statement::VariableDecl(
1648                ast::VariableDecl {
1649                    kind: VarKind::Let,
1650                    is_mut: false,
1651                    pattern: DestructurePattern::Identifier("shared".to_string(), span()),
1652                    type_annotation: None,
1653                    value: Some(Expr::Reference {
1654                        expr: Box::new(Expr::Identifier("left".to_string(), span())),
1655                        is_mutable: false,
1656                        span: span(),
1657                    }),
1658                    ownership: OwnershipModifier::Inferred,
1659                },
1660                span(),
1661            ),
1662            Statement::Assignment(
1663                ast::Assignment {
1664                    pattern: DestructurePattern::Identifier("left".to_string(), span()),
1665                    value: Expr::Literal(ast::Literal::Int(3), span()),
1666                },
1667                span(),
1668            ),
1669            Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
1670        ];
1671        let lowering = lower_function_detailed("test", &[pair_param], &body, span());
1672        assert!(!lowering.had_fallbacks);
1673        let analysis = solver::analyze(&lowering.mir, &Default::default());
1674        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1675    }
1676
1677    #[test]
1678    fn test_lowered_destructure_rest_pattern_write_while_borrowed_is_visible_to_solver() {
1679        let lowering = lower_parsed_function(
1680            r#"
1681                function test(items) {
1682                    var [head, ...tail] = items
1683                    let shared = &tail
1684                    tail = items
1685                    shared
1686                }
1687            "#,
1688        );
1689        assert!(!lowering.had_fallbacks);
1690        let analysis = solver::analyze(&lowering.mir, &Default::default());
1691        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1692    }
1693
1694    #[test]
1695    fn test_lowered_decomposition_pattern_write_while_borrowed_is_visible_to_solver() {
1696        let lowering = lower_parsed_function(
1697            r#"
1698                function test(merged) {
1699                    var (left: {x}, right: {y}) = merged
1700                    let shared = &left
1701                    left = merged
1702                    shared
1703                }
1704            "#,
1705        );
1706        assert!(!lowering.had_fallbacks);
1707        let analysis = solver::analyze(&lowering.mir, &Default::default());
1708        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1709    }
1710
1711    #[test]
1712    fn test_lowered_supported_runtime_opaque_expressions_stay_supported() {
1713        let mut overrides = std::collections::HashMap::new();
1714        overrides.insert(
1715            "digits".to_string(),
1716            Expr::Literal(ast::Literal::Int(2), span()),
1717        );
1718        let body = vec![
1719            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()),
1720            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()),
1721            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()),
1722            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()),
1723            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()),
1724            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()),
1725            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()),
1726            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()),
1727            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()),
1728            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()),
1729            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()),
1730            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()),
1731            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()),
1732            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()),
1733            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()),
1734            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()),
1735            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()),
1736        ];
1737        let lowering = lower_function_detailed("test", &[], &body, span());
1738        assert!(!lowering.had_fallbacks);
1739    }
1740
1741    #[test]
1742    fn test_lowered_assignment_expr_write_while_borrowed_is_visible_to_solver() {
1743        let body = vec![
1744            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()),
1745            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()),
1746            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()),
1747            Statement::Return(Some(Expr::Identifier("shared".to_string(), span())), span()),
1748        ];
1749        let lowering = lower_function_detailed("test", &[], &body, span());
1750        assert!(!lowering.had_fallbacks);
1751        let analysis = solver::analyze(&lowering.mir, &Default::default());
1752        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1753    }
1754
1755    #[test]
1756    fn test_lowered_property_assignment_expr_preserves_disjoint_places() {
1757        let body = vec![
1758            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()),
1759            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()),
1760            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()),
1761        ];
1762        let lowering = lower_function_detailed("test", &[], &body, span());
1763        assert!(!lowering.had_fallbacks);
1764        let analysis = solver::analyze(&lowering.mir, &Default::default());
1765        assert!(analysis.errors.is_empty());
1766    }
1767
1768    #[test]
1769    fn test_lowered_property_assignment_direct_ref_escape_is_visible_to_solver() {
1770        let lowering = lower_parsed_function(r#"
1771            function test() {
1772                var obj = { value: 0 }
1773                let x = 1
1774                obj.value = &x
1775                0
1776            }
1777        "#);
1778        assert!(!lowering.had_fallbacks);
1779        let analysis = solver::analyze(&lowering.mir, &Default::default());
1780        assert!(analysis.errors.is_empty());
1781    }
1782
1783    #[test]
1784    fn test_lowered_property_assignment_indirect_ref_escape_is_visible_to_solver() {
1785        let lowering = lower_parsed_function(r#"
1786            function test() {
1787                var obj = { value: 0 }
1788                let x = 1
1789                let r = &x
1790                obj.value = r
1791                0
1792            }
1793        "#);
1794        assert!(!lowering.had_fallbacks);
1795        let analysis = solver::analyze(&lowering.mir, &Default::default());
1796        assert!(analysis.errors.is_empty());
1797    }
1798
1799    #[test]
1800    fn test_lowered_index_assignment_direct_ref_escape_is_visible_to_solver() {
1801        let lowering = lower_parsed_function(r#"
1802            function test() {
1803                var arr = [0]
1804                let x = 1
1805                arr[0] = &x
1806                0
1807            }
1808        "#);
1809        assert!(!lowering.had_fallbacks);
1810        let analysis = solver::analyze(&lowering.mir, &Default::default());
1811        assert!(analysis.errors.is_empty());
1812    }
1813
1814    #[test]
1815    fn test_lowered_index_assignment_indirect_ref_escape_is_visible_to_solver() {
1816        let lowering = lower_parsed_function(r#"
1817            function test() {
1818                var arr = [0]
1819                let x = 1
1820                let r = &x
1821                arr[0] = r
1822                0
1823            }
1824        "#);
1825        assert!(!lowering.had_fallbacks);
1826        let analysis = solver::analyze(&lowering.mir, &Default::default());
1827        assert!(analysis.errors.is_empty());
1828    }
1829
1830    #[test]
1831    fn test_lowered_block_expr_write_while_borrowed_is_visible_to_solver() {
1832        let body = vec![
1833            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()),
1834            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()),
1835            Statement::Assignment(ast::Assignment { pattern: DestructurePattern::Identifier("x".to_string(), span()), value: Expr::Literal(ast::Literal::Int(2), span()) }, span()),
1836            Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
1837        ];
1838        let lowering = lower_function_detailed("test", &[], &body, span());
1839        assert!(!lowering.had_fallbacks);
1840        let analysis = solver::analyze(&lowering.mir, &Default::default());
1841        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1842    }
1843
1844    #[test]
1845    fn test_lowered_let_expr_write_while_borrowed_is_visible_to_solver() {
1846        let body = vec![
1847            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()),
1848            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()),
1849            Statement::Assignment(ast::Assignment { pattern: DestructurePattern::Identifier("x".to_string(), span()), value: Expr::Literal(ast::Literal::Int(2), span()) }, span()),
1850            Statement::Expression(Expr::Identifier("shared".to_string(), span()), span()),
1851        ];
1852        let lowering = lower_function_detailed("test", &[], &body, span());
1853        assert!(!lowering.had_fallbacks);
1854        let analysis = solver::analyze(&lowering.mir, &Default::default());
1855        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
1856    }
1857
1858    #[test]
1859    fn test_lowered_if_expression_with_block_branches_stays_supported() {
1860        let block_branch = |borrow_name: &str| {
1861            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())
1862        };
1863        let body = vec![
1864            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()),
1865            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()),
1866            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()),
1867            Statement::Assignment(ast::Assignment { pattern: DestructurePattern::Identifier("x".to_string(), span()), value: Expr::Literal(ast::Literal::Int(2), span()) }, span()),
1868        ];
1869        let lowering = lower_function_detailed("test", &[], &body, span());
1870        assert!(!lowering.had_fallbacks);
1871        let analysis = solver::analyze(&lowering.mir, &Default::default());
1872        assert!(analysis.errors.is_empty());
1873    }
1874
1875    #[test]
1876    fn test_lowered_async_let_exclusive_ref_task_boundary_is_visible_to_solver() {
1877        let lowering = lower_parsed_function(r#"
1878            async function test() {
1879                let mut x = 1
1880                async let fut = &mut x
1881            }
1882        "#);
1883        assert!(!lowering.had_fallbacks);
1884        let analysis = solver::analyze(&lowering.mir, &Default::default());
1885        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
1886    }
1887
1888    #[test]
1889    fn test_lowered_async_let_nested_ref_binding_task_boundary_is_visible_to_solver() {
1890        let lowering = lower_parsed_function(r#"
1891            async function test() {
1892                let mut x = 1
1893                async let fut = {
1894                    let r = &mut x
1895                    r
1896                }
1897            }
1898        "#);
1899        assert!(!lowering.had_fallbacks);
1900        let analysis = solver::analyze(&lowering.mir, &Default::default());
1901        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
1902    }
1903
1904    #[test]
1905    fn test_lowered_async_let_shared_ref_task_boundary_stays_clean() {
1906        let lowering = lower_parsed_function(r#"
1907            async function test() {
1908                let x = 1
1909                async let fut = &x
1910                await fut
1911            }
1912        "#);
1913        assert!(!lowering.had_fallbacks);
1914        let analysis = solver::analyze(&lowering.mir, &Default::default());
1915        assert!(!analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
1916    }
1917
1918    #[test]
1919    fn test_lowered_join_exclusive_ref_task_boundary_is_visible_to_solver() {
1920        let lowering = lower_parsed_function(r#"
1921            async function test() {
1922                let mut x = 1
1923                await join all {
1924                    &mut x,
1925                    2,
1926                }
1927            }
1928        "#);
1929        assert!(!lowering.had_fallbacks);
1930        let analysis = solver::analyze(&lowering.mir, &Default::default());
1931        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ExclusiveRefAcrossTaskBoundary));
1932    }
1933
1934    #[test]
1935    fn test_lowered_async_scope_with_async_let_stays_supported() {
1936        let lowering = lower_parsed_function(r#"
1937            async function test() {
1938                let x = 1
1939                async scope {
1940                    async let fut = &x
1941                    await fut
1942                }
1943            }
1944        "#);
1945        assert!(!lowering.had_fallbacks);
1946        let analysis = solver::analyze(&lowering.mir, &Default::default());
1947        assert!(analysis.errors.is_empty());
1948    }
1949
1950    #[test]
1951    fn test_lowered_closure_capture_of_reference_is_visible_to_solver() {
1952        let lowering = lower_parsed_function(r#"
1953            function test() {
1954                let x = 1
1955                let r = &x
1956                let f = || r
1957            }
1958        "#);
1959        assert!(!lowering.had_fallbacks);
1960        let analysis = solver::analyze(&lowering.mir, &Default::default());
1961        assert!(analysis.errors.is_empty());
1962    }
1963
1964    #[test]
1965    fn test_lowered_returned_array_with_ref_still_errors() {
1966        let lowering = lower_parsed_function(r#"
1967            function test() {
1968                let x = 1
1969                let arr = [&x]
1970                return arr
1971            }
1972        "#);
1973        assert!(!lowering.had_fallbacks);
1974        let analysis = solver::analyze(&lowering.mir, &Default::default());
1975        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ReferenceStoredInArray));
1976    }
1977
1978    #[test]
1979    fn test_lowered_returned_closure_with_ref_still_errors() {
1980        let lowering = lower_parsed_function(r#"
1981            function test() {
1982                let x = 1
1983                let r = &x
1984                let f = || r
1985                return f
1986            }
1987        "#);
1988        assert!(!lowering.had_fallbacks);
1989        let analysis = solver::analyze(&lowering.mir, &Default::default());
1990        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::ReferenceEscapeIntoClosure));
1991    }
1992
1993    #[test]
1994    fn test_lowered_closure_capture_of_owned_value_stays_clean() {
1995        let lowering = lower_parsed_function(r#"
1996            function test() {
1997                let x = 1
1998                let f = || x
1999            }
2000        "#);
2001        assert!(!lowering.had_fallbacks);
2002        let analysis = solver::analyze(&lowering.mir, &Default::default());
2003        assert!(analysis.errors.is_empty());
2004    }
2005
2006    #[test]
2007    fn test_lowered_list_comprehension_write_conflict_is_visible_to_solver() {
2008        let lowering = lower_parsed_function(r#"
2009            function test() {
2010                let mut x = 1
2011                let r = &x
2012                let xs = [(x = 2) for y in [1]]
2013                r
2014            }
2015        "#);
2016        assert!(!lowering.had_fallbacks);
2017        let analysis = solver::analyze(&lowering.mir, &Default::default());
2018        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
2019    }
2020
2021    #[test]
2022    fn test_lowered_from_query_write_conflict_is_visible_to_solver() {
2023        let lowering = lower_parsed_function(r#"
2024            function test() {
2025                let mut x = 1
2026                let r = &x
2027                let rows = from y in [1] where (x = 2) > 0 select y
2028                r
2029            }
2030        "#);
2031        assert!(!lowering.had_fallbacks);
2032        let analysis = solver::analyze(&lowering.mir, &Default::default());
2033        assert!(analysis.errors.iter().any(|error| error.kind == BorrowErrorKind::WriteWhileBorrowed));
2034    }
2035
2036    #[test]
2037    fn test_lowered_comptime_expr_stays_supported() {
2038        let lowering = lower_parsed_function(r#"
2039            function test() {
2040                let generated = comptime {
2041                    let x = 1
2042                }
2043            }
2044        "#);
2045        assert!(!lowering.had_fallbacks);
2046    }
2047
2048    #[test]
2049    fn test_lowered_comptime_for_expr_stays_supported() {
2050        let lowering = lower_parsed_function(r#"
2051            function test() {
2052                let generated = comptime for f in [1, 2] {
2053                    let y = f
2054                }
2055            }
2056        "#);
2057        assert!(!lowering.had_fallbacks);
2058    }
2059}