Skip to main content

bock_types/
ownership.rs

1//! Ownership analysis — O-AIR pass.
2//!
3//! Implements tree-walk ownership analysis over the AIR with
4//! divergence-aware branch merging. Detects:
5//!
6//! - Use-after-move errors
7//! - Mutable borrows of non-`mut` variables
8//! - Moves inside loop bodies (would double-move on next iteration)
9//!
10//! # Algorithm
11//!
12//! Walk the AIR, maintaining a per-variable [`VarOwnership`] map. At
13//! control-flow join points (if/else, match, guard) merge branch states:
14//! diverging branches are excluded; among non-diverging branches, any move
15//! makes the variable considered moved at the join.
16
17use std::collections::{HashMap, HashSet};
18
19use bock_air::stubs::Value;
20use bock_air::{AIRNode, AirInterpolationPart, NodeId, NodeKind};
21use bock_ast::AssignOp;
22use bock_errors::{DiagnosticBag, DiagnosticCode, Span};
23
24// ─── Diagnostic codes ─────────────────────────────────────────────────────────
25
26const E_USE_AFTER_MOVE: DiagnosticCode = DiagnosticCode {
27    prefix: 'E',
28    number: 5001,
29};
30const E_MUT_BORROW_NEEDS_MUT: DiagnosticCode = DiagnosticCode {
31    prefix: 'E',
32    number: 5002,
33};
34const E_LOOP_MOVE: DiagnosticCode = DiagnosticCode {
35    prefix: 'E',
36    number: 5003,
37};
38
39// ─── Public types ─────────────────────────────────────────────────────────────
40
41/// Convenience alias — the AIR module root node.
42pub type AIRModule = AIRNode;
43
44/// Ownership state of a variable at a given program point.
45#[derive(Debug, Clone, PartialEq)]
46pub enum OwnershipState {
47    /// Variable owns its value.
48    Owned,
49    /// Variable is currently immutably borrowed.
50    Borrowed,
51    /// Variable is currently mutably borrowed.
52    MutBorrowed,
53    /// Value has been moved out; variable is invalid.
54    Moved,
55    /// Annotated `@managed` — GC semantics, no tracking.
56    Managed,
57}
58
59/// Ownership information for a single binding at a program point.
60#[derive(Debug, Clone, PartialEq)]
61pub struct OwnershipInfo {
62    /// Current ownership state.
63    pub state: OwnershipState,
64    /// Whether the binding was declared `mut`.
65    pub mutable: bool,
66    /// Node that established ownership (the binding site).
67    pub origin: NodeId,
68}
69
70// ─── Internal bookkeeping ─────────────────────────────────────────────────────
71
72#[derive(Clone)]
73struct VarOwnership {
74    state: OwnershipState,
75    is_mut: bool,
76    /// The span where the value was moved, if it has been.
77    move_site: Option<Span>,
78}
79
80// ─── Analyzer ────────────────────────────────────────────────────────────────
81
82struct OwnershipAnalyzer {
83    diags: DiagnosticBag,
84    env: HashMap<String, VarOwnership>,
85    in_loop: bool,
86    /// Variable names that existed when the current loop was entered.
87    loop_entry_keys: HashSet<String>,
88    /// Whether we are inside a `@managed` function body.
89    in_managed: bool,
90}
91
92impl OwnershipAnalyzer {
93    fn new() -> Self {
94        Self {
95            diags: DiagnosticBag::new(),
96            env: HashMap::new(),
97            in_loop: false,
98            loop_entry_keys: HashSet::new(),
99            in_managed: false,
100        }
101    }
102
103    fn snapshot(&self) -> HashMap<String, VarOwnership> {
104        self.env.clone()
105    }
106
107    /// Check that `name` is still valid (not moved). Emits a diagnostic if it
108    /// has been moved.
109    fn check_use(&mut self, name: &str, use_span: Span) {
110        if let Some(var) = self.env.get(name) {
111            if matches!(var.state, OwnershipState::Moved) {
112                let move_site = var.move_site;
113                let diag = self.diags.error(
114                    E_USE_AFTER_MOVE,
115                    format!("use of moved variable `{name}`"),
116                    use_span,
117                );
118                if let Some(ms) = move_site {
119                    diag.label(ms, "value moved here");
120                }
121            }
122        }
123    }
124
125    /// Mark `name` as moved from `move_span`. Emits loop-move or
126    /// double-move diagnostics as needed.
127    fn do_move(&mut self, name: &str, move_span: Span) {
128        if let Some(var) = self.env.get(name) {
129            if matches!(var.state, OwnershipState::Managed) {
130                return; // @managed — no tracking
131            }
132        }
133
134        // Moving a pre-loop variable inside a loop body = double-move error.
135        if self.in_loop && self.loop_entry_keys.contains(name) {
136            self.diags.error(
137                E_LOOP_MOVE,
138                format!(
139                    "cannot move `{name}` inside a loop \
140                     (would be moved on every iteration)"
141                ),
142                move_span,
143            );
144        }
145
146        if let Some(var) = self.env.get_mut(name) {
147            if matches!(var.state, OwnershipState::Moved) {
148                // Already moved — use-after-move.
149                let move_site = var.move_site;
150                let diag = self.diags.error(
151                    E_USE_AFTER_MOVE,
152                    format!("use of moved variable `{name}`"),
153                    move_span,
154                );
155                if let Some(ms) = move_site {
156                    diag.label(ms, "value moved here");
157                }
158            } else {
159                var.state = OwnershipState::Moved;
160                var.move_site = Some(move_span);
161            }
162        }
163    }
164
165    /// Analyze a node whose produced value will be *moved* (transferred).
166    ///
167    /// For bare identifiers this marks the variable as moved; for everything
168    /// else it delegates to the normal analysis (the value comes from a
169    /// fresh temporary and nothing in the env is moved).
170    fn analyze_move(&mut self, node: &AIRNode) -> bool {
171        if let NodeKind::Identifier { name } = &node.kind {
172            if let Some(var) = self.env.get(&name.name) {
173                if matches!(var.state, OwnershipState::Managed) {
174                    return false; // @managed: skip
175                }
176            }
177            // Primitive types (Int, Float, Bool, Char, String) have copy
178            // semantics — using them doesn't transfer ownership.
179            if node.metadata.get("copy_type") == Some(&Value::Bool(true)) {
180                return false;
181            }
182            self.do_move(&name.name, node.span);
183            false
184        } else {
185            self.analyze_node(node)
186        }
187    }
188
189    /// Merge branch states after a fork.
190    ///
191    /// `pre` is the state at the fork entry. `branches` is a list of
192    /// (diverges, post-state) pairs. Diverging branches are excluded.
193    /// If all branches diverge the join is unreachable and `pre` is returned.
194    fn merge_states(
195        &self,
196        pre: &HashMap<String, VarOwnership>,
197        branches: &[(bool, HashMap<String, VarOwnership>)],
198    ) -> HashMap<String, VarOwnership> {
199        let non_div: Vec<&HashMap<String, VarOwnership>> = branches
200            .iter()
201            .filter(|(div, _)| !*div)
202            .map(|(_, s)| s)
203            .collect();
204
205        if non_div.is_empty() {
206            // Unreachable join — propagate pre-state unchanged.
207            return pre.clone();
208        }
209
210        let mut result = pre.clone();
211        for name in pre.keys() {
212            let any_moved = non_div.iter().any(|state| {
213                state
214                    .get(name)
215                    .is_some_and(|v| matches!(v.state, OwnershipState::Moved))
216            });
217            if any_moved {
218                if let Some(var) = result.get_mut(name) {
219                    let move_site = non_div
220                        .iter()
221                        .filter_map(|state| state.get(name))
222                        .find(|v| matches!(v.state, OwnershipState::Moved))
223                        .and_then(|v| v.move_site);
224                    var.state = OwnershipState::Moved;
225                    var.move_site = move_site;
226                }
227            }
228        }
229        result
230    }
231
232    /// Add a parameter as an owned binding.
233    fn bind_param(&mut self, param: &AIRNode) {
234        if let NodeKind::Param { pattern, .. } = &param.kind {
235            if let NodeKind::BindPat { name, is_mut } = &pattern.kind {
236                let state = if self.in_managed {
237                    OwnershipState::Managed
238                } else {
239                    OwnershipState::Owned
240                };
241                self.env.insert(
242                    name.name.clone(),
243                    VarOwnership {
244                        state,
245                        is_mut: *is_mut,
246                        move_site: None,
247                    },
248                );
249            }
250        }
251    }
252
253    /// Add bindings introduced by a pattern (e.g. in `let`, match arms, loops).
254    fn bind_pattern(&mut self, pat: &AIRNode) {
255        let base_state = if self.in_managed {
256            OwnershipState::Managed
257        } else {
258            OwnershipState::Owned
259        };
260        match &pat.kind {
261            NodeKind::BindPat { name, is_mut } => {
262                self.env.insert(
263                    name.name.clone(),
264                    VarOwnership {
265                        state: base_state,
266                        is_mut: *is_mut,
267                        move_site: None,
268                    },
269                );
270            }
271            NodeKind::TuplePat { elems } => {
272                for e in elems {
273                    self.bind_pattern(e);
274                }
275            }
276            NodeKind::ConstructorPat { fields, .. } => {
277                for f in fields {
278                    self.bind_pattern(f);
279                }
280            }
281            NodeKind::RecordPat { fields, .. } => {
282                for f in fields {
283                    if let Some(p) = &f.pattern {
284                        self.bind_pattern(p);
285                    } else {
286                        self.env.insert(
287                            f.name.name.clone(),
288                            VarOwnership {
289                                state: base_state.clone(),
290                                is_mut: false,
291                                move_site: None,
292                            },
293                        );
294                    }
295                }
296            }
297            NodeKind::ListPat { elems, rest } => {
298                for e in elems {
299                    self.bind_pattern(e);
300                }
301                if let Some(r) = rest {
302                    self.bind_pattern(r);
303                }
304            }
305            NodeKind::OrPat { alternatives } => {
306                if let Some(first) = alternatives.first() {
307                    self.bind_pattern(first);
308                }
309            }
310            _ => {}
311        }
312    }
313
314    /// Returns `true` if this node always diverges (never returns normally).
315    #[allow(clippy::too_many_lines)]
316    fn analyze_node(&mut self, node: &AIRNode) -> bool {
317        match &node.kind {
318            // ── Module root ──────────────────────────────────────────────────
319            NodeKind::Module { imports, items, .. } => {
320                for n in imports {
321                    self.analyze_node(n);
322                }
323                for n in items {
324                    self.analyze_node(n);
325                }
326                false
327            }
328
329            // ── Declarations ─────────────────────────────────────────────────
330            NodeKind::FnDecl {
331                annotations,
332                params,
333                body,
334                ..
335            } => {
336                let outer = self.snapshot();
337                let outer_managed = self.in_managed;
338                if annotations.iter().any(|a| a.name.name == "managed") {
339                    self.in_managed = true;
340                }
341                for p in params {
342                    self.bind_param(p);
343                }
344                self.analyze_node(body);
345                self.env = outer;
346                self.in_managed = outer_managed;
347                false
348            }
349            NodeKind::ImplBlock { methods, .. } => {
350                for m in methods {
351                    self.analyze_node(m);
352                }
353                false
354            }
355            NodeKind::ClassDecl { methods, .. } => {
356                for m in methods {
357                    self.analyze_node(m);
358                }
359                false
360            }
361            NodeKind::TraitDecl { methods, .. } => {
362                for m in methods {
363                    self.analyze_node(m);
364                }
365                false
366            }
367            // Type-level / effect decls carry no runtime ownership.
368            NodeKind::RecordDecl { .. }
369            | NodeKind::EnumDecl { .. }
370            | NodeKind::TypeAlias { .. }
371            | NodeKind::EffectDecl { .. }
372            | NodeKind::ConstDecl { .. }
373            | NodeKind::ImportDecl { .. }
374            | NodeKind::ModuleHandle { .. }
375            | NodeKind::PropertyTest { .. } => false,
376
377            // ── Block ─────────────────────────────────────────────────────────
378            NodeKind::Block { stmts, tail } => {
379                let pre_keys: HashSet<String> = self.env.keys().cloned().collect();
380                let mut diverges = false;
381                for stmt in stmts {
382                    if diverges {
383                        break;
384                    }
385                    diverges = self.analyze_node(stmt);
386                }
387                if !diverges {
388                    if let Some(t) = tail {
389                        diverges = self.analyze_node(t);
390                    }
391                }
392                // Drop bindings that were introduced in this block scope.
393                self.env.retain(|k, _| pre_keys.contains(k));
394                diverges
395            }
396
397            // ── Let binding ───────────────────────────────────────────────────
398            NodeKind::LetBinding {
399                is_mut,
400                pattern,
401                value,
402                ..
403            } => {
404                let is_managed = self.in_managed
405                    || node.metadata.get("managed") == Some(&Value::Bool(true));
406
407                self.analyze_move(value);
408
409                let state = if is_managed {
410                    OwnershipState::Managed
411                } else {
412                    OwnershipState::Owned
413                };
414
415                // For tracking purposes we only need to insert simple BindPat
416                // names; complex patterns are handled by bind_pattern which
417                // also adds Owned state. @managed simply overrides.
418                let own = VarOwnership {
419                    state,
420                    is_mut: *is_mut,
421                    move_site: None,
422                };
423                if is_managed {
424                    // Insert with Managed state so reads are still fine.
425                    if let NodeKind::BindPat { name, .. } = &pattern.kind {
426                        self.env.insert(name.name.clone(), own);
427                    } else {
428                        self.bind_pattern(pattern);
429                        // Override all inserted entries to Managed.
430                        // (Nested managed patterns are uncommon; this is best-effort.)
431                    }
432                } else {
433                    self.bind_pattern(pattern);
434                }
435                false
436            }
437
438            // ── Assignment ────────────────────────────────────────────────────
439            NodeKind::Assign { op, target, value } => {
440                match op {
441                    AssignOp::Assign => {
442                        // Plain assignment: rhs value is moved into lhs.
443                        self.analyze_move(value);
444                        self.analyze_node(target);
445                    }
446                    _ => {
447                        // Compound assignment: both are reads.
448                        self.analyze_node(target);
449                        self.analyze_node(value);
450                    }
451                }
452                false
453            }
454
455            // ── Identifier use ────────────────────────────────────────────────
456            NodeKind::Identifier { name } => {
457                self.check_use(&name.name, node.span);
458                false
459            }
460
461            // ── Explicit ownership operations ─────────────────────────────────
462            NodeKind::Move { expr } => {
463                self.analyze_move(expr);
464                false
465            }
466            NodeKind::Borrow { expr } => {
467                // Immutable borrow — check validity, no ownership change.
468                self.analyze_node(expr);
469                false
470            }
471            NodeKind::MutableBorrow { expr } => {
472                // Mutable borrow requires `mut` on the binding.
473                if let NodeKind::Identifier { name } = &expr.kind {
474                    if let Some(var) = self.env.get(&name.name) {
475                        if !var.is_mut && !matches!(var.state, OwnershipState::Managed) {
476                            self.diags.error(
477                                E_MUT_BORROW_NEEDS_MUT,
478                                format!(
479                                    "cannot mutably borrow `{}`: \
480                                     variable not declared `mut`",
481                                    name.name
482                                ),
483                                expr.span,
484                            );
485                        }
486                    }
487                }
488                self.analyze_node(expr);
489                false
490            }
491
492            // ── Diverging statements ──────────────────────────────────────────
493            NodeKind::Return { value } => {
494                if let Some(v) = value {
495                    // Return exits the function (and any enclosing loop),
496                    // so moves in the return value cannot repeat.
497                    let old_in_loop = self.in_loop;
498                    self.in_loop = false;
499                    self.analyze_move(v);
500                    self.in_loop = old_in_loop;
501                }
502                true
503            }
504            NodeKind::Break { value } => {
505                if let Some(v) = value {
506                    // Break exits the loop, so moves in the break value
507                    // cannot repeat on the next iteration.
508                    let old_in_loop = self.in_loop;
509                    self.in_loop = false;
510                    self.analyze_move(v);
511                    self.in_loop = old_in_loop;
512                }
513                true
514            }
515            NodeKind::Continue => true,
516            NodeKind::Unreachable => true,
517
518            // ── If / if-let ───────────────────────────────────────────────────
519            NodeKind::If {
520                condition,
521                then_block,
522                else_block,
523                ..
524            } => {
525                self.analyze_node(condition);
526
527                let pre = self.snapshot();
528
529                let then_div = self.analyze_node(then_block);
530                let then_state = self.snapshot();
531
532                self.env = pre.clone();
533                let (else_div, else_state) = match else_block {
534                    Some(eb) => {
535                        let d = self.analyze_node(eb);
536                        (d, self.snapshot())
537                    }
538                    None => {
539                        // No else branch = implicitly non-diverging with pre-state.
540                        (false, pre.clone())
541                    }
542                };
543
544                self.env =
545                    self.merge_states(&pre, &[(then_div, then_state), (else_div, else_state)]);
546                then_div && else_div
547            }
548
549            // ── Guard ─────────────────────────────────────────────────────────
550            NodeKind::Guard {
551                let_pattern,
552                condition,
553                else_block,
554            } => {
555                if let Some(pat) = let_pattern {
556                    self.analyze_node(pat);
557                }
558                self.analyze_node(condition);
559                // The else_block should diverge; even if it doesn't we still
560                // exclude its state from the main path per spec.
561                let pre = self.snapshot();
562                self.analyze_node(else_block);
563                // Main path continues from pre-state (else excluded).
564                self.env = pre;
565                false
566            }
567
568            // ── Match ─────────────────────────────────────────────────────────
569            NodeKind::Match { scrutinee, arms } => {
570                self.analyze_node(scrutinee);
571
572                let pre = self.snapshot();
573                let mut arm_results: Vec<(bool, HashMap<String, VarOwnership>)> =
574                    Vec::with_capacity(arms.len());
575
576                for arm in arms {
577                    self.env = pre.clone();
578                    let div = self.analyze_node(arm);
579                    arm_results.push((div, self.snapshot()));
580                }
581
582                self.env = self.merge_states(&pre, &arm_results);
583                arm_results.iter().all(|(d, _)| *d)
584            }
585
586            NodeKind::MatchArm {
587                pattern,
588                guard,
589                body,
590            } => {
591                let pre_keys: HashSet<String> = self.env.keys().cloned().collect();
592                self.bind_pattern(pattern);
593                if let Some(g) = guard {
594                    self.analyze_node(g);
595                }
596                let div = self.analyze_node(body);
597                // Drop arm-local bindings.
598                self.env.retain(|k, _| pre_keys.contains(k));
599                div
600            }
601
602            // ── Loops ─────────────────────────────────────────────────────────
603            NodeKind::For {
604                pattern,
605                iterable,
606                body,
607            } => {
608                self.analyze_node(iterable);
609                let pre = self.snapshot();
610                let old_in_loop = self.in_loop;
611                let old_loop_keys = std::mem::take(&mut self.loop_entry_keys);
612                self.in_loop = true;
613                self.loop_entry_keys = pre.keys().cloned().collect();
614                self.bind_pattern(pattern);
615                self.analyze_node(body);
616                self.in_loop = old_in_loop;
617                self.loop_entry_keys = old_loop_keys;
618                // Loop may execute zero times — restore pre-state.
619                self.env = pre;
620                false
621            }
622            NodeKind::While { condition, body } => {
623                self.analyze_node(condition);
624                let pre = self.snapshot();
625                let old_in_loop = self.in_loop;
626                let old_loop_keys = std::mem::take(&mut self.loop_entry_keys);
627                self.in_loop = true;
628                self.loop_entry_keys = pre.keys().cloned().collect();
629                self.analyze_node(body);
630                self.in_loop = old_in_loop;
631                self.loop_entry_keys = old_loop_keys;
632                self.env = pre;
633                false
634            }
635            NodeKind::Loop { body } => {
636                let pre = self.snapshot();
637                let old_in_loop = self.in_loop;
638                let old_loop_keys = std::mem::take(&mut self.loop_entry_keys);
639                self.in_loop = true;
640                self.loop_entry_keys = pre.keys().cloned().collect();
641                self.analyze_node(body);
642                self.in_loop = old_in_loop;
643                self.loop_entry_keys = old_loop_keys;
644                self.env = pre;
645                false
646            }
647
648            // ── Calls ─────────────────────────────────────────────────────────
649            NodeKind::Call { callee, args, .. } => {
650                self.analyze_node(callee);
651                for arg in args {
652                    // Arguments are borrows by default; explicit `move` or
653                    // `MutableBorrow` wrapping is handled by their own arms.
654                    self.analyze_node(&arg.value);
655                }
656                false
657            }
658            NodeKind::MethodCall { receiver, args, .. } => {
659                self.analyze_node(receiver);
660                for arg in args {
661                    self.analyze_node(&arg.value);
662                }
663                false
664            }
665
666            // ── Lambda ────────────────────────────────────────────────────────
667            NodeKind::Lambda { params, body } => {
668                let outer = self.snapshot();
669                for p in params {
670                    self.bind_param(p);
671                }
672                self.analyze_node(body);
673                self.env = outer;
674                false
675            }
676
677            // ── Other expressions ─────────────────────────────────────────────
678            NodeKind::BinaryOp { left, right, .. } => {
679                self.analyze_node(left);
680                self.analyze_node(right);
681                false
682            }
683            NodeKind::UnaryOp { operand, .. } => {
684                self.analyze_node(operand);
685                false
686            }
687            NodeKind::FieldAccess { object, .. } => {
688                self.analyze_node(object);
689                false
690            }
691            NodeKind::Index { object, index } => {
692                self.analyze_node(object);
693                self.analyze_node(index);
694                false
695            }
696            NodeKind::Propagate { expr } => {
697                self.analyze_node(expr);
698                false
699            }
700            NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
701                self.analyze_node(left);
702                self.analyze_node(right);
703                false
704            }
705            NodeKind::Await { expr } => {
706                self.analyze_node(expr);
707                false
708            }
709            NodeKind::Range { lo, hi, .. } => {
710                self.analyze_node(lo);
711                self.analyze_node(hi);
712                false
713            }
714            NodeKind::RecordConstruct { fields, spread, .. } => {
715                for f in fields {
716                    if let Some(v) = &f.value {
717                        self.analyze_move(v);
718                    }
719                }
720                if let Some(s) = spread {
721                    self.analyze_node(s);
722                }
723                false
724            }
725            NodeKind::ListLiteral { elems }
726            | NodeKind::SetLiteral { elems }
727            | NodeKind::TupleLiteral { elems } => {
728                for e in elems {
729                    self.analyze_move(e);
730                }
731                false
732            }
733            NodeKind::MapLiteral { entries } => {
734                for entry in entries {
735                    self.analyze_move(&entry.key);
736                    self.analyze_move(&entry.value);
737                }
738                false
739            }
740            NodeKind::Interpolation { parts } => {
741                for part in parts {
742                    if let AirInterpolationPart::Expr(e) = part {
743                        self.analyze_node(e);
744                    }
745                }
746                false
747            }
748            NodeKind::ResultConstruct { value, .. } => {
749                if let Some(v) = value {
750                    self.analyze_move(v);
751                }
752                false
753            }
754            NodeKind::HandlingBlock { handlers, body } => {
755                for h in handlers {
756                    self.analyze_node(&h.handler);
757                }
758                self.analyze_node(body);
759                false
760            }
761
762            // ── Terminals ─────────────────────────────────────────────────────
763            NodeKind::Literal { .. }
764            | NodeKind::Placeholder
765            | NodeKind::TypeSelf
766            | NodeKind::WildcardPat
767            | NodeKind::LiteralPat { .. }
768            | NodeKind::RestPat
769            | NodeKind::TypeNamed { .. }
770            | NodeKind::TypeTuple { .. }
771            | NodeKind::TypeFunction { .. }
772            | NodeKind::TypeOptional { .. }
773            | NodeKind::EffectOp { .. }
774            | NodeKind::EffectRef { .. }
775            | NodeKind::Error => false,
776
777            // ── Patterns (visited via bind_pattern, not analyze_node) ─────────
778            NodeKind::BindPat { .. }
779            | NodeKind::ConstructorPat { .. }
780            | NodeKind::RecordPat { .. }
781            | NodeKind::TuplePat { .. }
782            | NodeKind::ListPat { .. }
783            | NodeKind::OrPat { .. }
784            | NodeKind::GuardPat { .. }
785            | NodeKind::RangePat { .. } => false,
786
787            // ── Catch-all for future node kinds ──────────────────────────────
788            _ => false,
789        }
790    }
791}
792
793// ─── Public entry point ───────────────────────────────────────────────────────
794
795/// Perform ownership analysis on an AIR module.
796///
797/// Returns a [`DiagnosticBag`] containing any ownership violations found.
798/// A non-empty bag with errors indicates the program has ownership errors.
799///
800/// # Analysis performed
801///
802/// - **Use-after-move**: using a variable after its value was moved out.
803/// - **Mutable borrow of non-`mut`**: `&mut x` when `x` was not declared `mut`.
804/// - **Move in loop body**: moving a pre-loop variable inside a loop.
805/// - **`@managed` escape hatch**: variables with `metadata["managed"] = true`
806///   are excluded from ownership tracking.
807/// - **Divergence-aware branch merging**: diverging branches (return, break,
808///   continue, unreachable) are excluded from join-point state merges.
809#[must_use]
810pub fn analyze_ownership(module: &AIRModule) -> DiagnosticBag {
811    let mut analyzer = OwnershipAnalyzer::new();
812    analyzer.analyze_node(module);
813    analyzer.diags
814}
815
816// ─── Tests ────────────────────────────────────────────────────────────────────
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821    use bock_air::stubs::Value;
822    use bock_air::{AIRNode, AirArg, NodeIdGen, NodeKind};
823    use bock_ast::{Ident, Literal};
824    use bock_errors::{FileId, Span};
825
826    // ── Helpers ───────────────────────────────────────────────────────────────
827
828    fn span() -> Span {
829        Span {
830            file: FileId(0),
831            start: 0,
832            end: 0,
833        }
834    }
835
836    fn span_at(start: usize, end: usize) -> Span {
837        Span {
838            file: FileId(0),
839            start,
840            end,
841        }
842    }
843
844    fn ident(name: &str) -> Ident {
845        Ident {
846            name: name.to_string(),
847            span: span(),
848        }
849    }
850
851    fn node(gen: &NodeIdGen, kind: NodeKind) -> AIRNode {
852        AIRNode::new(gen.next(), span(), kind)
853    }
854
855    fn node_at(gen: &NodeIdGen, kind: NodeKind, s: usize, e: usize) -> AIRNode {
856        AIRNode::new(gen.next(), span_at(s, e), kind)
857    }
858
859    fn id_node(gen: &NodeIdGen, name: &str) -> AIRNode {
860        node(gen, NodeKind::Identifier { name: ident(name) })
861    }
862
863    fn id_node_at(gen: &NodeIdGen, name: &str, s: usize, e: usize) -> AIRNode {
864        node_at(gen, NodeKind::Identifier { name: ident(name) }, s, e)
865    }
866
867    fn lit_node(gen: &NodeIdGen) -> AIRNode {
868        node(
869            gen,
870            NodeKind::Literal {
871                lit: Literal::Int("42".into()),
872            },
873        )
874    }
875
876    fn bind_pat(gen: &NodeIdGen, name: &str, is_mut: bool) -> AIRNode {
877        node(
878            gen,
879            NodeKind::BindPat {
880                name: ident(name),
881                is_mut,
882            },
883        )
884    }
885
886    fn let_binding(gen: &NodeIdGen, name: &str, is_mut: bool, value: AIRNode) -> AIRNode {
887        node(
888            gen,
889            NodeKind::LetBinding {
890                is_mut,
891                pattern: Box::new(bind_pat(gen, name, is_mut)),
892                ty: None,
893                value: Box::new(value),
894            },
895        )
896    }
897
898    fn block(gen: &NodeIdGen, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
899        node(
900            gen,
901            NodeKind::Block {
902                stmts,
903                tail: tail.map(Box::new),
904            },
905        )
906    }
907
908    fn module(gen: &NodeIdGen, items: Vec<AIRNode>) -> AIRNode {
909        node(
910            gen,
911            NodeKind::Module {
912                path: None,
913                annotations: vec![],
914                imports: vec![],
915                items,
916            },
917        )
918    }
919
920    fn fn_decl(gen: &NodeIdGen, body: AIRNode) -> AIRNode {
921        fn_decl_with(gen, body, vec![])
922    }
923
924    fn managed_fn_decl(gen: &NodeIdGen, body: AIRNode) -> AIRNode {
925        use bock_ast::Annotation;
926        fn_decl_with(
927            gen,
928            body,
929            vec![Annotation {
930                id: 0,
931                span: span(),
932                name: ident("managed"),
933                args: vec![],
934            }],
935        )
936    }
937
938    fn fn_decl_with(gen: &NodeIdGen, body: AIRNode, annotations: Vec<bock_ast::Annotation>) -> AIRNode {
939        node(
940            gen,
941            NodeKind::FnDecl {
942                annotations,
943                visibility: bock_ast::Visibility::Public,
944                is_async: false,
945                name: ident("f"),
946                generic_params: vec![],
947                params: vec![],
948                return_type: None,
949                effect_clause: vec![],
950                where_clause: vec![],
951                body: Box::new(body),
952            },
953        )
954    }
955
956    fn return_node(gen: &NodeIdGen, val: Option<AIRNode>) -> AIRNode {
957        node(
958            gen,
959            NodeKind::Return {
960                value: val.map(Box::new),
961            },
962        )
963    }
964
965    fn move_node(gen: &NodeIdGen, expr: AIRNode) -> AIRNode {
966        node(
967            gen,
968            NodeKind::Move {
969                expr: Box::new(expr),
970            },
971        )
972    }
973
974    fn mut_borrow(gen: &NodeIdGen, expr: AIRNode) -> AIRNode {
975        node(
976            gen,
977            NodeKind::MutableBorrow {
978                expr: Box::new(expr),
979            },
980        )
981    }
982
983    fn if_node(gen: &NodeIdGen, cond: AIRNode, then: AIRNode, else_: Option<AIRNode>) -> AIRNode {
984        node(
985            gen,
986            NodeKind::If {
987                let_pattern: None,
988                condition: Box::new(cond),
989                then_block: Box::new(then),
990                else_block: else_.map(Box::new),
991            },
992        )
993    }
994
995    fn guard_node(gen: &NodeIdGen, cond: AIRNode, else_block: AIRNode) -> AIRNode {
996        node(
997            gen,
998            NodeKind::Guard {
999                let_pattern: None,
1000                condition: Box::new(cond),
1001                else_block: Box::new(else_block),
1002            },
1003        )
1004    }
1005
1006    fn loop_node(gen: &NodeIdGen, body: AIRNode) -> AIRNode {
1007        node(
1008            gen,
1009            NodeKind::Loop {
1010                body: Box::new(body),
1011            },
1012        )
1013    }
1014
1015    fn match_node(gen: &NodeIdGen, scrutinee: AIRNode, arms: Vec<AIRNode>) -> AIRNode {
1016        node(
1017            gen,
1018            NodeKind::Match {
1019                scrutinee: Box::new(scrutinee),
1020                arms,
1021            },
1022        )
1023    }
1024
1025    fn match_arm(gen: &NodeIdGen, pat: AIRNode, body: AIRNode) -> AIRNode {
1026        node(
1027            gen,
1028            NodeKind::MatchArm {
1029                pattern: Box::new(pat),
1030                guard: None,
1031                body: Box::new(body),
1032            },
1033        )
1034    }
1035
1036    // ── Tests: move detection ─────────────────────────────────────────────────
1037
1038    #[test]
1039    fn no_error_simple_borrow() {
1040        // let data = 42
1041        // summarize(data)   -- borrow, data still owned
1042        // use(data)         -- still ok
1043        let gen = NodeIdGen::new();
1044        let data_lit = lit_node(&gen);
1045        let let_data = let_binding(&gen, "data", false, data_lit);
1046        let use1 = id_node(&gen, "data");
1047        let use2 = id_node(&gen, "data");
1048        let call = node(
1049            &gen,
1050            NodeKind::Call {
1051                callee: Box::new(id_node(&gen, "summarize")),
1052                args: vec![AirArg {
1053                    label: None,
1054                    value: use1,
1055                }],
1056                type_args: vec![],
1057            },
1058        );
1059        let b = block(&gen, vec![let_data, call, use2], None);
1060        let m = module(&gen, vec![fn_decl(&gen, b)]);
1061        let diags = analyze_ownership(&m);
1062        assert!(
1063            !diags.has_errors(),
1064            "expected no errors, got: {:?}",
1065            diags.iter().collect::<Vec<_>>()
1066        );
1067    }
1068
1069    #[test]
1070    fn move_on_let_binding() {
1071        // let data = 42
1072        // let archive = data   -- moves data
1073        // use(data)            -- ERROR: use after move
1074        let gen = NodeIdGen::new();
1075        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1076        let id_data = id_node_at(&gen, "data", 10, 14);
1077        let let_archive = let_binding(&gen, "archive", false, id_data);
1078        let use_data = id_node_at(&gen, "data", 20, 24);
1079        let b = block(&gen, vec![let_data, let_archive, use_data], None);
1080        let m = module(&gen, vec![fn_decl(&gen, b)]);
1081        let diags = analyze_ownership(&m);
1082        assert!(diags.has_errors());
1083        assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
1084    }
1085
1086    #[test]
1087    fn explicit_move_node() {
1088        // let data = 42
1089        // move data
1090        // use data  -- ERROR
1091        let gen = NodeIdGen::new();
1092        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1093        let mv = move_node(&gen, id_node_at(&gen, "data", 5, 9));
1094        let use_data = id_node_at(&gen, "data", 15, 19);
1095        let b = block(&gen, vec![let_data, mv, use_data], None);
1096        let m = module(&gen, vec![fn_decl(&gen, b)]);
1097        let diags = analyze_ownership(&m);
1098        assert!(diags.has_errors());
1099        assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
1100    }
1101
1102    #[test]
1103    fn use_after_move_has_move_site_label() {
1104        let gen = NodeIdGen::new();
1105        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1106        let mv = move_node(&gen, id_node_at(&gen, "data", 5, 9));
1107        let use_data = id_node_at(&gen, "data", 15, 19);
1108        let b = block(&gen, vec![let_data, mv, use_data], None);
1109        let m = module(&gen, vec![fn_decl(&gen, b)]);
1110        let diags = analyze_ownership(&m);
1111        let err = diags.iter().find(|d| d.code == E_USE_AFTER_MOVE).unwrap();
1112        assert!(
1113            !err.labels.is_empty(),
1114            "expected a label pointing to move site"
1115        );
1116        assert!(err.labels[0].message.contains("moved"));
1117    }
1118
1119    // ── Tests: mutable borrow ─────────────────────────────────────────────────
1120
1121    #[test]
1122    fn mut_borrow_of_non_mut_errors() {
1123        // let data = 42       -- not mut
1124        // &mut data           -- ERROR
1125        let gen = NodeIdGen::new();
1126        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1127        let mb = mut_borrow(&gen, id_node(&gen, "data"));
1128        let b = block(&gen, vec![let_data, mb], None);
1129        let m = module(&gen, vec![fn_decl(&gen, b)]);
1130        let diags = analyze_ownership(&m);
1131        assert!(diags.has_errors());
1132        assert!(diags.iter().any(|d| d.code == E_MUT_BORROW_NEEDS_MUT));
1133    }
1134
1135    #[test]
1136    fn mut_borrow_of_mut_ok() {
1137        // let mut data = 42
1138        // &mut data           -- OK
1139        let gen = NodeIdGen::new();
1140        let let_data = let_binding(&gen, "data", true, lit_node(&gen));
1141        let mb = mut_borrow(&gen, id_node(&gen, "data"));
1142        let b = block(&gen, vec![let_data, mb], None);
1143        let m = module(&gen, vec![fn_decl(&gen, b)]);
1144        let diags = analyze_ownership(&m);
1145        assert!(!diags.has_errors());
1146    }
1147
1148    // ── Tests: @managed ───────────────────────────────────────────────────────
1149
1150    #[test]
1151    fn managed_skips_ownership_tracking() {
1152        // @managed let data = 42  (metadata["managed"] = true)
1153        // let archive = data       -- would be a move error, but @managed skips
1154        // use data                 -- no error
1155        let gen = NodeIdGen::new();
1156        let mut let_data = let_binding(&gen, "data", false, lit_node(&gen));
1157        let_data
1158            .metadata
1159            .insert("managed".into(), Value::Bool(true));
1160        let id_data = id_node(&gen, "data");
1161        let let_archive = let_binding(&gen, "archive", false, id_data);
1162        let use_data = id_node(&gen, "data");
1163        let b = block(&gen, vec![let_data, let_archive, use_data], None);
1164        let m = module(&gen, vec![fn_decl(&gen, b)]);
1165        let diags = analyze_ownership(&m);
1166        assert!(!diags.has_errors());
1167    }
1168
1169    #[test]
1170    fn managed_fn_suppresses_move_errors() {
1171        // @managed
1172        // fn f() {
1173        //   let data = 42
1174        //   let a = data   -- move
1175        //   let b = data   -- would be use-after-move, but @managed suppresses
1176        // }
1177        let gen = NodeIdGen::new();
1178        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1179        let id_data1 = id_node(&gen, "data");
1180        let let_a = let_binding(&gen, "a", false, id_data1);
1181        let id_data2 = id_node(&gen, "data");
1182        let let_b = let_binding(&gen, "b", false, id_data2);
1183        let b = block(&gen, vec![let_data, let_a, let_b], None);
1184        let m = module(&gen, vec![managed_fn_decl(&gen, b)]);
1185        let diags = analyze_ownership(&m);
1186        assert!(
1187            !diags.has_errors(),
1188            "expected no errors in @managed fn, got: {:?}",
1189            diags.iter().collect::<Vec<_>>()
1190        );
1191    }
1192
1193    #[test]
1194    fn non_managed_fn_still_errors_on_reuse() {
1195        // fn f() {
1196        //   let data = 42
1197        //   let a = data
1198        //   let b = data   -- ERROR: use after move
1199        // }
1200        let gen = NodeIdGen::new();
1201        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1202        let id_data1 = id_node(&gen, "data");
1203        let let_a = let_binding(&gen, "a", false, id_data1);
1204        let id_data2 = id_node_at(&gen, "data", 20, 24);
1205        let let_b = let_binding(&gen, "b", false, id_data2);
1206        let b = block(&gen, vec![let_data, let_a, let_b], None);
1207        let m = module(&gen, vec![fn_decl(&gen, b)]);
1208        let diags = analyze_ownership(&m);
1209        assert!(diags.has_errors());
1210        assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
1211    }
1212
1213    #[test]
1214    fn managed_fn_loop_move_suppressed() {
1215        // @managed
1216        // fn f() {
1217        //   let data = 42
1218        //   loop { let _ = data }   -- would be E5003 loop-move, but @managed suppresses
1219        // }
1220        let gen = NodeIdGen::new();
1221        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1222        let id_data = id_node(&gen, "data");
1223        let let_discard = let_binding(&gen, "_d", false, id_data);
1224        let loop_body = block(&gen, vec![let_discard], None);
1225        let lp = loop_node(&gen, loop_body);
1226        let b = block(&gen, vec![let_data, lp], None);
1227        let m = module(&gen, vec![managed_fn_decl(&gen, b)]);
1228        let diags = analyze_ownership(&m);
1229        assert!(
1230            !diags.has_errors(),
1231            "expected no errors in @managed fn loop, got: {:?}",
1232            diags.iter().collect::<Vec<_>>()
1233        );
1234    }
1235
1236    // ── Tests: diverging branches ─────────────────────────────────────────────
1237
1238    #[test]
1239    fn guard_else_diverges_data_still_owned() {
1240        // guard (cond) else { return }   -- else diverges
1241        // use data                        -- data still owned
1242        let gen = NodeIdGen::new();
1243        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1244        let cond = lit_node(&gen);
1245        let else_block = block(&gen, vec![return_node(&gen, None)], None);
1246        let guard = guard_node(&gen, cond, else_block);
1247        let use_data = id_node(&gen, "data");
1248        let b = block(&gen, vec![let_data, guard, use_data], None);
1249        let m = module(&gen, vec![fn_decl(&gen, b)]);
1250        let diags = analyze_ownership(&m);
1251        assert!(!diags.has_errors());
1252    }
1253
1254    #[test]
1255    fn if_with_diverging_then_data_still_owned() {
1256        // let data = 42
1257        // if (cond) { consume(data); return }   -- then diverges
1258        // use data                               -- data still owned
1259        let gen = NodeIdGen::new();
1260        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1261        let cond = lit_node(&gen);
1262        // then: move data then return
1263        let id_data = id_node(&gen, "data");
1264        let let_archive = let_binding(&gen, "archive", false, id_data);
1265        let ret = return_node(&gen, None);
1266        let then_block = block(&gen, vec![let_archive, ret], None);
1267        let if_node_ = if_node(&gen, cond, then_block, None);
1268        let use_data = id_node(&gen, "data");
1269        let b = block(&gen, vec![let_data, if_node_, use_data], None);
1270        let m = module(&gen, vec![fn_decl(&gen, b)]);
1271        let diags = analyze_ownership(&m);
1272        assert!(!diags.has_errors());
1273    }
1274
1275    #[test]
1276    fn if_non_diverging_branch_moves_makes_moved_at_join() {
1277        // let data = 42
1278        // if (cond) { let archive = data }   -- non-diverging, moves data
1279        // use data                            -- ERROR
1280        let gen = NodeIdGen::new();
1281        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1282        let cond = lit_node(&gen);
1283        let id_data = id_node(&gen, "data");
1284        let let_archive = let_binding(&gen, "archive", false, id_data);
1285        let then_block = block(&gen, vec![let_archive], None);
1286        let if_node_ = if_node(&gen, cond, then_block, None);
1287        let use_data = id_node_at(&gen, "data", 30, 34);
1288        let b = block(&gen, vec![let_data, if_node_, use_data], None);
1289        let m = module(&gen, vec![fn_decl(&gen, b)]);
1290        let diags = analyze_ownership(&m);
1291        assert!(diags.has_errors());
1292        assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
1293    }
1294
1295    #[test]
1296    fn if_both_branches_diverge_join_uses_pre_state() {
1297        // let data = 42
1298        // if (cond) { return } else { return }  -- both diverge
1299        // (unreachable join — no error)
1300        let gen = NodeIdGen::new();
1301        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1302        let cond = lit_node(&gen);
1303        let then_block = block(&gen, vec![return_node(&gen, None)], None);
1304        let else_block = block(&gen, vec![return_node(&gen, None)], None);
1305        let if_node_ = if_node(&gen, cond, then_block, Some(else_block));
1306        let b = block(&gen, vec![let_data, if_node_], None);
1307        let m = module(&gen, vec![fn_decl(&gen, b)]);
1308        let diags = analyze_ownership(&m);
1309        assert!(!diags.has_errors());
1310    }
1311
1312    // ── Tests: loop move detection ────────────────────────────────────────────
1313
1314    #[test]
1315    fn move_inside_loop_is_error() {
1316        // let data = 42
1317        // loop { let archive = data }   -- ERROR: moving pre-loop var in loop
1318        let gen = NodeIdGen::new();
1319        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1320        let id_data = id_node_at(&gen, "data", 10, 14);
1321        let let_archive = let_binding(&gen, "archive", false, id_data);
1322        let loop_body = block(&gen, vec![let_archive], None);
1323        let lp = loop_node(&gen, loop_body);
1324        let b = block(&gen, vec![let_data, lp], None);
1325        let m = module(&gen, vec![fn_decl(&gen, b)]);
1326        let diags = analyze_ownership(&m);
1327        assert!(diags.has_errors());
1328        assert!(diags.iter().any(|d| d.code == E_LOOP_MOVE));
1329    }
1330
1331    #[test]
1332    fn variable_defined_inside_loop_can_be_moved() {
1333        // loop { let tmp = 42; let _ = tmp }  -- OK: tmp is fresh each iteration
1334        let gen = NodeIdGen::new();
1335        let let_tmp = let_binding(&gen, "tmp", false, lit_node(&gen));
1336        let id_tmp = id_node(&gen, "tmp");
1337        let let_discard = let_binding(&gen, "_unused", false, id_tmp);
1338        let loop_body = block(&gen, vec![let_tmp, let_discard], None);
1339        let lp = loop_node(&gen, loop_body);
1340        let b = block(&gen, vec![lp], None);
1341        let m = module(&gen, vec![fn_decl(&gen, b)]);
1342        let diags = analyze_ownership(&m);
1343        assert!(!diags.has_errors());
1344    }
1345
1346    // ── Tests: match with diverging arms ─────────────────────────────────────
1347
1348    #[test]
1349    fn match_all_arms_diverge_no_use_after_move() {
1350        // let data = 42
1351        // match x {
1352        //   Ok(v) => { let _ = data; return }   -- diverges
1353        //   Err(_) => return                     -- diverges
1354        // }
1355        // (unreachable join)
1356        let gen = NodeIdGen::new();
1357        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1358        let scrutinee = lit_node(&gen);
1359        let id_data = id_node(&gen, "data");
1360        let let_discard = let_binding(&gen, "_d", false, id_data);
1361        let ret1 = return_node(&gen, None);
1362        let arm1_body = block(&gen, vec![let_discard, ret1], None);
1363        let arm1 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm1_body);
1364        let ret2 = return_node(&gen, None);
1365        let arm2_body = block(&gen, vec![ret2], None);
1366        let arm2 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm2_body);
1367        let m_node = match_node(&gen, scrutinee, vec![arm1, arm2]);
1368        let b = block(&gen, vec![let_data, m_node], None);
1369        let m = module(&gen, vec![fn_decl(&gen, b)]);
1370        let diags = analyze_ownership(&m);
1371        assert!(!diags.has_errors());
1372    }
1373
1374    #[test]
1375    fn match_non_diverging_arm_moves_is_error_after() {
1376        // let data = 42
1377        // match x {
1378        //   A => { let _ = data }  -- non-diverging, moves data
1379        //   B => return             -- diverges
1380        // }
1381        // use data  -- ERROR: moved in non-diverging arm
1382        let gen = NodeIdGen::new();
1383        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1384        let scrutinee = lit_node(&gen);
1385        let id_data = id_node(&gen, "data");
1386        let let_discard = let_binding(&gen, "_d", false, id_data);
1387        let arm1_body = block(&gen, vec![let_discard], None);
1388        let arm1 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm1_body);
1389        let ret = return_node(&gen, None);
1390        let arm2_body = block(&gen, vec![ret], None);
1391        let arm2 = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm2_body);
1392        let m_node = match_node(&gen, scrutinee, vec![arm1, arm2]);
1393        let use_data = id_node_at(&gen, "data", 50, 54);
1394        let b = block(&gen, vec![let_data, m_node, use_data], None);
1395        let m = module(&gen, vec![fn_decl(&gen, b)]);
1396        let diags = analyze_ownership(&m);
1397        assert!(diags.has_errors());
1398        assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
1399    }
1400
1401    // ── Tests: nested control flow ────────────────────────────────────────────
1402
1403    #[test]
1404    fn if_inside_match_arm_nested() {
1405        // let data = 42
1406        // match x {
1407        //   _ => if (cond) { let _ = data } else { return }
1408        //        -- then: non-div, moves data; else: diverges
1409        //        -- after if: data is moved (from non-div branch)
1410        // }
1411        // use data  -- ERROR
1412        let gen = NodeIdGen::new();
1413        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1414        let scrutinee = lit_node(&gen);
1415        let cond = lit_node(&gen);
1416        let id_data = id_node(&gen, "data");
1417        let let_discard = let_binding(&gen, "_d", false, id_data);
1418        let then_block = block(&gen, vec![let_discard], None);
1419        let else_block = block(&gen, vec![return_node(&gen, None)], None);
1420        let if_expr = if_node(&gen, cond, then_block, Some(else_block));
1421        let arm_body = block(&gen, vec![if_expr], None);
1422        let arm = match_arm(&gen, node(&gen, NodeKind::WildcardPat), arm_body);
1423        let m_node = match_node(&gen, scrutinee, vec![arm]);
1424        let use_data = id_node_at(&gen, "data", 50, 54);
1425        let b = block(&gen, vec![let_data, m_node, use_data], None);
1426        let m = module(&gen, vec![fn_decl(&gen, b)]);
1427        let diags = analyze_ownership(&m);
1428        assert!(diags.has_errors());
1429        assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
1430    }
1431
1432    #[test]
1433    fn no_false_positive_if_else_both_leave_owned() {
1434        // let data = 42
1435        // if (cond) { use(data) } else { use(data) }  -- borrows in both branches
1436        // use data  -- still owned, no error
1437        let gen = NodeIdGen::new();
1438        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1439        let cond = lit_node(&gen);
1440        let use1 = id_node(&gen, "data");
1441        let use2 = id_node(&gen, "data");
1442        let then_block = block(&gen, vec![use1], None);
1443        let else_block = block(&gen, vec![use2], None);
1444        let if_expr = if_node(&gen, cond, then_block, Some(else_block));
1445        let use_after = id_node(&gen, "data");
1446        let b = block(&gen, vec![let_data, if_expr, use_after], None);
1447        let m = module(&gen, vec![fn_decl(&gen, b)]);
1448        let diags = analyze_ownership(&m);
1449        assert!(!diags.has_errors());
1450    }
1451
1452    #[test]
1453    fn double_move_error() {
1454        // let data = 42
1455        // let a = data   -- moves data
1456        // let b = data   -- ERROR: use after move
1457        let gen = NodeIdGen::new();
1458        let let_data = let_binding(&gen, "data", false, lit_node(&gen));
1459        let let_a = let_binding(&gen, "a", false, id_node(&gen, "data"));
1460        let let_b = let_binding(&gen, "b", false, id_node_at(&gen, "data", 20, 24));
1461        let b = block(&gen, vec![let_data, let_a, let_b], None);
1462        let m = module(&gen, vec![fn_decl(&gen, b)]);
1463        let diags = analyze_ownership(&m);
1464        assert!(diags.has_errors());
1465        assert!(diags.iter().any(|d| d.code == E_USE_AFTER_MOVE));
1466    }
1467}