Skip to main content

asupersync/obligation/
leak_check.rs

1//! Static obligation leak checker via abstract interpretation.
2//!
3//! Walks a structured obligation IR and detects paths where obligations
4//! may be leaked (scope exit while still held).
5
6use crate::record::ObligationKind;
7use std::collections::{HashMap, HashSet};
8use std::fmt;
9
10// ============================================================================
11// ObligationVar
12// ============================================================================
13
14/// Identifies an obligation variable in the IR.
15///
16/// Variables are lightweight handles: `ObligationVar(0)`, `ObligationVar(1)`, etc.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct ObligationVar(pub u32);
19
20impl fmt::Display for ObligationVar {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        write!(f, "v{}", self.0)
23    }
24}
25
26// ============================================================================
27// VarState (Abstract Domain)
28// ============================================================================
29
30/// Abstract state of a single obligation variable.
31///
32/// Lattice for forward dataflow:
33/// ```text
34///           MayHold(K)
35///          /         \
36///     Held(K)     Resolved
37///          \         /
38///           Empty
39/// ```
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum VarState {
42    /// No obligation held.
43    Empty,
44    /// Definitely holds an obligation of this kind.
45    Held(ObligationKind),
46    /// May hold an obligation (depends on control flow).
47    MayHold(ObligationKind),
48    /// May hold an obligation, but the kind is ambiguous (different paths had different kinds).
49    MayHoldAmbiguous,
50    /// Obligation has been resolved (committed or aborted).
51    Resolved,
52}
53
54impl VarState {
55    /// Join two abstract states (lattice join for forward analysis).
56    ///
57    /// Used when control flow paths merge (e.g., after an if/else).
58    #[must_use]
59    pub fn join(self, other: Self) -> Self {
60        use VarState::{Empty, Held, MayHold, MayHoldAmbiguous, Resolved};
61        match (self, other) {
62            // Identity cases.
63            (Empty, Empty) => Empty,
64            (Resolved | Empty, Resolved) | (Resolved, Empty) => Resolved,
65
66            // Same kinds.
67            (Held(k1), Held(k2)) if k1 == k2 => Held(k1),
68            (MayHold(k1), MayHold(k2)) if k1 == k2 => MayHold(k1),
69            (Held(k1), MayHold(k2)) | (MayHold(k2), Held(k1)) if k1 == k2 => MayHold(k1),
70
71            // Held in one path, not in another => MayHold.
72            (Held(k) | MayHold(k), Resolved | Empty) | (Resolved | Empty, Held(k) | MayHold(k)) => {
73                MayHold(k)
74            }
75
76            // Ambiguous cases (mismatched kinds or existing ambiguity).
77            (MayHoldAmbiguous, _)
78            | (_, MayHoldAmbiguous)
79            | (Held(_) | MayHold(_), Held(_) | MayHold(_)) => MayHoldAmbiguous,
80        }
81    }
82
83    /// Returns true if this state indicates a potential leak.
84    #[must_use]
85    pub fn is_leak(&self) -> bool {
86        matches!(
87            self,
88            Self::Held(_) | Self::MayHold(_) | Self::MayHoldAmbiguous
89        )
90    }
91
92    /// Returns the obligation kind, if any.
93    #[must_use]
94    pub fn kind(&self) -> Option<ObligationKind> {
95        match self {
96            Self::Held(k) | Self::MayHold(k) => Some(*k),
97            Self::Empty | Self::Resolved | Self::MayHoldAmbiguous => None,
98        }
99    }
100}
101
102impl fmt::Display for VarState {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            Self::Empty => f.write_str("empty"),
106            Self::Held(k) => write!(f, "held({k})"),
107            Self::MayHold(k) => write!(f, "may-hold({k})"),
108            Self::MayHoldAmbiguous => f.write_str("may-hold(ambiguous)"),
109            Self::Resolved => f.write_str("resolved"),
110        }
111    }
112}
113
114// ============================================================================
115// Instruction (IR)
116// ============================================================================
117
118/// An instruction in the obligation IR.
119///
120/// The IR is structured (not a CFG): branches are nested, which simplifies
121/// the prototype checker while covering the key patterns.
122#[derive(Debug, Clone)]
123pub enum Instruction {
124    /// Reserve an obligation: var becomes `Held(kind)`.
125    Reserve {
126        /// Variable to bind.
127        var: ObligationVar,
128        /// Obligation kind.
129        kind: ObligationKind,
130    },
131    /// Commit (resolve) an obligation: var becomes `Resolved`.
132    Commit {
133        /// Variable to resolve.
134        var: ObligationVar,
135    },
136    /// Abort (resolve) an obligation: var becomes `Resolved`.
137    Abort {
138        /// Variable to resolve.
139        var: ObligationVar,
140    },
141    /// Conditional branch: each arm is a sequence of instructions.
142    /// After the branch, abstract states from all arms are joined.
143    Branch {
144        /// Branch arms (e.g., if/else = 2 arms, match = N arms).
145        arms: Vec<Vec<Self>>,
146    },
147}
148
149// ============================================================================
150// Body
151// ============================================================================
152
153/// A function body to check.
154///
155/// Contains a name (for diagnostics) and a sequence of instructions.
156#[derive(Debug, Clone)]
157pub struct Body {
158    /// Name of the function/scope being checked.
159    pub name: String,
160    /// Instructions in program order.
161    pub instructions: Vec<Instruction>,
162}
163
164impl Body {
165    /// Creates a new body with the given name and instructions.
166    #[must_use]
167    pub fn new(name: impl Into<String>, instructions: Vec<Instruction>) -> Self {
168        Self {
169            name: name.into(),
170            instructions,
171        }
172    }
173}
174
175// ============================================================================
176// Diagnostics
177// ============================================================================
178
179/// Diagnostic severity/kind.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum DiagnosticKind {
182    /// Obligation is definitely leaked (held at scope exit in all paths).
183    DefiniteLeak,
184    /// Obligation may be leaked (held in some but not all paths).
185    PotentialLeak,
186    /// Obligation resolved twice.
187    DoubleResolve,
188    /// Resolve on a variable that was never reserved.
189    ResolveUnheld,
190}
191
192impl fmt::Display for DiagnosticKind {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            Self::DefiniteLeak => f.write_str("definite-leak"),
196            Self::PotentialLeak => f.write_str("potential-leak"),
197            Self::DoubleResolve => f.write_str("double-resolve"),
198            Self::ResolveUnheld => f.write_str("resolve-unheld"),
199        }
200    }
201}
202
203/// A diagnostic emitted by the checker.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct Diagnostic {
206    /// What kind of issue.
207    pub kind: DiagnosticKind,
208    /// The variable involved.
209    pub var: ObligationVar,
210    /// The obligation kind, if known.
211    pub obligation_kind: Option<ObligationKind>,
212    /// The function/scope name where the issue was found.
213    pub scope: String,
214    /// Human-readable message.
215    pub message: String,
216}
217
218impl fmt::Display for Diagnostic {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        write!(
221            f,
222            "[{}] {} in `{}`: {}",
223            self.kind, self.var, self.scope, self.message
224        )
225    }
226}
227
228// ============================================================================
229// CheckResult
230// ============================================================================
231
232/// Result of checking a body.
233#[derive(Debug, Clone)]
234pub struct CheckResult {
235    /// The function/scope checked.
236    pub scope: String,
237    /// Diagnostics found.
238    pub diagnostics: Vec<Diagnostic>,
239}
240
241impl CheckResult {
242    /// Returns true if no issues were found.
243    #[must_use]
244    pub fn is_clean(&self) -> bool {
245        self.diagnostics.is_empty()
246    }
247
248    /// Returns only leak diagnostics (definite + potential).
249    #[must_use]
250    pub fn leaks(&self) -> Vec<&Diagnostic> {
251        self.diagnostics
252            .iter()
253            .filter(|d| {
254                matches!(
255                    d.kind,
256                    DiagnosticKind::DefiniteLeak | DiagnosticKind::PotentialLeak
257                )
258            })
259            .collect()
260    }
261
262    /// Returns only double-resolve diagnostics.
263    #[must_use]
264    pub fn double_resolves(&self) -> Vec<&Diagnostic> {
265        self.diagnostics
266            .iter()
267            .filter(|d| d.kind == DiagnosticKind::DoubleResolve)
268            .collect()
269    }
270}
271
272impl fmt::Display for CheckResult {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        if self.is_clean() {
275            write!(f, "`{}`: no issues", self.scope)
276        } else {
277            writeln!(
278                f,
279                "`{}`: {} diagnostic(s)",
280                self.scope,
281                self.diagnostics.len()
282            )?;
283            for d in &self.diagnostics {
284                writeln!(f, "  {d}")?;
285            }
286            Ok(())
287        }
288    }
289}
290
291// ============================================================================
292// LeakChecker
293// ============================================================================
294
295/// The static obligation leak checker.
296///
297/// Performs abstract interpretation over a [`Body`] to detect obligation leaks.
298/// The checker maintains a map from [`ObligationVar`] to [`VarState`] and walks
299/// instructions in order, emitting [`Diagnostic`]s when issues are found.
300#[derive(Debug, Default)]
301pub struct LeakChecker {
302    /// Current abstract state: var → state.
303    state: HashMap<ObligationVar, VarState>,
304    /// Accumulated diagnostics.
305    diagnostics: Vec<Diagnostic>,
306    /// Current scope name (for diagnostic messages).
307    scope_name: String,
308}
309
310impl LeakChecker {
311    /// Creates a new checker.
312    #[must_use]
313    pub fn new() -> Self {
314        Self::default()
315    }
316
317    /// Check a body for obligation leaks.
318    ///
319    /// Returns a [`CheckResult`] with any diagnostics found. The checker
320    /// is reset before each invocation, so it can be reused across bodies.
321    #[must_use]
322    pub fn check(&mut self, body: &Body) -> CheckResult {
323        self.state.clear();
324        self.diagnostics.clear();
325        self.scope_name.clone_from(&body.name);
326
327        self.check_instructions(&body.instructions);
328        self.check_exit_leaks();
329
330        CheckResult {
331            scope: body.name.clone(),
332            diagnostics: self.diagnostics.clone(),
333        }
334    }
335
336    fn check_instructions(&mut self, instructions: &[Instruction]) {
337        for instr in instructions {
338            self.check_instruction(instr);
339        }
340    }
341
342    fn check_instruction(&mut self, instr: &Instruction) {
343        match instr {
344            Instruction::Reserve { var, kind } => {
345                // If var already holds an obligation, that's a leak (overwrite).
346                if let Some(existing) = self.state.get(var) {
347                    if existing.is_leak() {
348                        self.diagnostics.push(Diagnostic {
349                            kind: DiagnosticKind::DefiniteLeak,
350                            var: *var,
351                            obligation_kind: existing.kind(),
352                            scope: self.scope_name.clone(),
353                            message: format!(
354                                "{var} already holds {}, overwriting with new {} reserve",
355                                existing,
356                                kind.as_str(),
357                            ),
358                        });
359                    }
360                }
361                self.state.insert(*var, VarState::Held(*kind));
362            }
363
364            Instruction::Commit { var } | Instruction::Abort { var } => {
365                let action = if matches!(instr, Instruction::Commit { .. }) {
366                    "commit"
367                } else {
368                    "abort"
369                };
370                match self.state.get(var) {
371                    Some(VarState::Held(_) | VarState::MayHold(_) | VarState::MayHoldAmbiguous) => {
372                        self.state.insert(*var, VarState::Resolved);
373                    }
374                    Some(VarState::Resolved) => {
375                        self.diagnostics.push(Diagnostic {
376                            kind: DiagnosticKind::DoubleResolve,
377                            var: *var,
378                            obligation_kind: None,
379                            scope: self.scope_name.clone(),
380                            message: format!("{var} already resolved, {action} is redundant/error"),
381                        });
382                    }
383                    Some(VarState::Empty) | None => {
384                        self.diagnostics.push(Diagnostic {
385                            kind: DiagnosticKind::ResolveUnheld,
386                            var: *var,
387                            obligation_kind: None,
388                            scope: self.scope_name.clone(),
389                            message: format!("{var} was never reserved, cannot {action}"),
390                        });
391                    }
392                }
393            }
394
395            Instruction::Branch { arms } => {
396                self.check_branch(arms);
397            }
398        }
399    }
400
401    fn check_branch(&mut self, arms: &[Vec<Instruction>]) {
402        if arms.is_empty() {
403            return;
404        }
405
406        let entry_state = self.state.clone();
407        let mut arm_states: Vec<HashMap<ObligationVar, VarState>> = Vec::new();
408
409        // Analyze each arm independently, starting from the entry state.
410        for arm in arms {
411            self.state.clone_from(&entry_state);
412            self.check_instructions(arm);
413            arm_states.push(self.state.clone());
414        }
415
416        // Join all arm exit states.
417        self.state = Self::join_states(&arm_states);
418    }
419
420    fn join_states(
421        states: &[HashMap<ObligationVar, VarState>],
422    ) -> HashMap<ObligationVar, VarState> {
423        if states.is_empty() {
424            return HashMap::new();
425        }
426        if states.len() == 1 {
427            return states[0].clone();
428        }
429
430        // Collect all vars across all arms.
431        let all_vars: HashSet<ObligationVar> =
432            states.iter().flat_map(|s| s.keys().copied()).collect();
433
434        let mut result = HashMap::new();
435        for var in all_vars {
436            let mut joined = states[0].get(&var).copied().unwrap_or(VarState::Empty);
437            for s in &states[1..] {
438                let other = s.get(&var).copied().unwrap_or(VarState::Empty);
439                joined = joined.join(other);
440            }
441            result.insert(var, joined);
442        }
443
444        result
445    }
446
447    fn check_exit_leaks(&mut self) {
448        // Collect vars and sort by index for deterministic output.
449        let mut vars: Vec<(ObligationVar, VarState)> =
450            self.state.iter().map(|(v, s)| (*v, *s)).collect();
451        vars.sort_by_key(|(v, _)| v.0);
452
453        for (var, state) in vars {
454            match state {
455                VarState::Held(kind) => {
456                    self.diagnostics.push(Diagnostic {
457                        kind: DiagnosticKind::DefiniteLeak,
458                        var,
459                        obligation_kind: Some(kind),
460                        scope: self.scope_name.clone(),
461                        message: format!("{var} holds {} obligation at scope exit", kind.as_str(),),
462                    });
463                }
464                VarState::MayHold(kind) => {
465                    self.diagnostics.push(Diagnostic {
466                        kind: DiagnosticKind::PotentialLeak,
467                        var,
468                        obligation_kind: Some(kind),
469                        scope: self.scope_name.clone(),
470                        message: format!(
471                            "{var} may hold {} obligation at scope exit (depends on control flow)",
472                            kind.as_str(),
473                        ),
474                    });
475                }
476                VarState::MayHoldAmbiguous => {
477                    self.diagnostics.push(Diagnostic {
478                        kind: DiagnosticKind::PotentialLeak,
479                        var,
480                        obligation_kind: None,
481                        scope: self.scope_name.clone(),
482                        message: format!(
483                            "{var} may hold an ambiguous obligation at scope exit (different kinds on different paths)",
484                        ),
485                    });
486                }
487                VarState::Empty | VarState::Resolved => {}
488            }
489        }
490    }
491}
492
493// ============================================================================
494// BodyBuilder — fluent IR construction
495// ============================================================================
496
497/// Fluent builder for constructing obligation [`Body`] IR.
498///
499/// Bridges the gap between real obligation code patterns and the static
500/// checker's structured IR. Variables are auto-assigned incrementally.
501///
502/// # Example
503///
504/// ```
505/// use asupersync::obligation::BodyBuilder;
506/// use asupersync::record::ObligationKind;
507///
508/// let mut b = BodyBuilder::new("send_handler");
509/// let permit = b.reserve(ObligationKind::SendPermit);
510/// b.branch(|bb| {
511///     bb.arm(|a| { a.commit(permit); });
512///     bb.arm(|a| { a.abort(permit); });
513/// });
514/// let body = b.build();
515///
516/// let mut checker = asupersync::obligation::LeakChecker::new();
517/// let result = checker.check(&body);
518/// assert!(result.is_clean());
519/// ```
520#[derive(Debug)]
521pub struct BodyBuilder {
522    name: String,
523    instructions: Vec<Instruction>,
524    next_var: u32,
525}
526
527impl BodyBuilder {
528    /// Create a new builder for a scope/function with the given name.
529    #[must_use]
530    pub fn new(name: impl Into<String>) -> Self {
531        Self {
532            name: name.into(),
533            instructions: Vec::new(),
534            next_var: 0,
535        }
536    }
537
538    /// Reserve a new obligation, returning the auto-assigned variable.
539    pub fn reserve(&mut self, kind: ObligationKind) -> ObligationVar {
540        let var = ObligationVar(self.next_var);
541        self.next_var += 1;
542        self.instructions.push(Instruction::Reserve { var, kind });
543        var
544    }
545
546    /// Record a commit instruction for the given variable.
547    pub fn commit(&mut self, var: ObligationVar) -> &mut Self {
548        self.instructions.push(Instruction::Commit { var });
549        self
550    }
551
552    /// Record an abort instruction for the given variable.
553    pub fn abort(&mut self, var: ObligationVar) -> &mut Self {
554        self.instructions.push(Instruction::Abort { var });
555        self
556    }
557
558    /// Add a branch (if/else, match) with multiple arms.
559    ///
560    /// Each arm is built via the [`BranchBuilder`] callback.
561    pub fn branch(&mut self, build: impl FnOnce(&mut BranchBuilder)) -> &mut Self {
562        let mut bb = BranchBuilder { arms: Vec::new() };
563        build(&mut bb);
564        self.instructions
565            .push(Instruction::Branch { arms: bb.arms });
566        self
567    }
568
569    /// Consume the builder and produce a [`Body`].
570    #[must_use]
571    pub fn build(self) -> Body {
572        Body {
573            name: self.name,
574            instructions: self.instructions,
575        }
576    }
577
578    /// Returns the next variable index (useful for manual variable allocation).
579    #[must_use]
580    pub fn next_var_index(&self) -> u32 {
581        self.next_var
582    }
583}
584
585/// Builder for branch arms within a [`BodyBuilder::branch`] call.
586#[derive(Debug)]
587pub struct BranchBuilder {
588    arms: Vec<Vec<Instruction>>,
589}
590
591impl BranchBuilder {
592    /// Add a branch arm. The callback receives an [`ArmBuilder`] to populate
593    /// the arm's instructions.
594    pub fn arm(&mut self, build: impl FnOnce(&mut ArmBuilder)) -> &mut Self {
595        let mut ab = ArmBuilder {
596            instructions: Vec::new(),
597        };
598        build(&mut ab);
599        self.arms.push(ab.instructions);
600        self
601    }
602}
603
604/// Builder for a single branch arm's instruction sequence.
605#[derive(Debug)]
606pub struct ArmBuilder {
607    instructions: Vec<Instruction>,
608}
609
610impl ArmBuilder {
611    /// Record a commit in this arm.
612    pub fn commit(&mut self, var: ObligationVar) -> &mut Self {
613        self.instructions.push(Instruction::Commit { var });
614        self
615    }
616
617    /// Record an abort in this arm.
618    pub fn abort(&mut self, var: ObligationVar) -> &mut Self {
619        self.instructions.push(Instruction::Abort { var });
620        self
621    }
622
623    /// Reserve a new obligation within this arm (for obligations local to one branch).
624    pub fn reserve(&mut self, var: ObligationVar, kind: ObligationKind) -> &mut Self {
625        self.instructions.push(Instruction::Reserve { var, kind });
626        self
627    }
628
629    /// Add a nested branch within this arm.
630    pub fn branch(&mut self, build: impl FnOnce(&mut BranchBuilder)) -> &mut Self {
631        let mut bb = BranchBuilder { arms: Vec::new() };
632        build(&mut bb);
633        self.instructions
634            .push(Instruction::Branch { arms: bb.arms });
635        self
636    }
637}
638
639// ============================================================================
640// ObligationAnalyzer — record-then-check bridge
641// ============================================================================
642
643/// Records obligation operations and validates them via the static [`LeakChecker`].
644///
645/// Bridges real obligation usage patterns to the static checker. Construct an
646/// analyzer, call `reserve`/`commit`/`abort` as your code would, then call
647/// `check()` to run the leak analysis.
648///
649/// # Example
650///
651/// ```
652/// use asupersync::obligation::ObligationAnalyzer;
653/// use asupersync::record::ObligationKind;
654///
655/// let mut analyzer = ObligationAnalyzer::new("my_handler");
656/// let permit = analyzer.reserve(ObligationKind::SendPermit);
657/// analyzer.commit(permit);
658/// let result = analyzer.check();
659/// assert!(result.is_clean());
660/// ```
661#[derive(Debug)]
662pub struct ObligationAnalyzer {
663    builder: BodyBuilder,
664}
665
666impl ObligationAnalyzer {
667    /// Create a new analyzer for the given scope name.
668    #[must_use]
669    pub fn new(scope: impl Into<String>) -> Self {
670        Self {
671            builder: BodyBuilder::new(scope),
672        }
673    }
674
675    /// Record a reserve operation, returning the variable handle.
676    pub fn reserve(&mut self, kind: ObligationKind) -> ObligationVar {
677        self.builder.reserve(kind)
678    }
679
680    /// Record a commit operation.
681    pub fn commit(&mut self, var: ObligationVar) {
682        self.builder.commit(var);
683    }
684
685    /// Record an abort operation.
686    pub fn abort(&mut self, var: ObligationVar) {
687        self.builder.abort(var);
688    }
689
690    /// Record a branch with multiple arms.
691    pub fn branch(&mut self, build: impl FnOnce(&mut BranchBuilder)) {
692        self.builder.branch(build);
693    }
694
695    /// Run the leak checker and return the result.
696    #[must_use]
697    pub fn check(self) -> CheckResult {
698        let body = self.builder.build();
699        let mut checker = LeakChecker::new();
700        checker.check(&body)
701    }
702
703    /// Assert that the recorded operations have no leaks.
704    ///
705    /// # Panics
706    ///
707    /// Panics with diagnostic details if any leaks are found.
708    pub fn assert_clean(self) {
709        let result = self.check();
710        assert!(result.is_clean(), "obligation leak check failed:\n{result}");
711    }
712
713    /// Assert that the recorded operations have exactly `expected` leak diagnostics.
714    ///
715    /// # Panics
716    ///
717    /// Panics if the number of leaks doesn't match.
718    pub fn assert_leaks(self, expected: usize) {
719        let result = self.check();
720        let leaks = result.leaks();
721        assert_eq!(
722            leaks.len(),
723            expected,
724            "expected {expected} leak(s) but found {}:\n{result}",
725            leaks.len()
726        );
727    }
728}
729
730// ============================================================================
731// Macros — DSL for inline body construction + assertion
732// ============================================================================
733
734/// Construct an obligation [`Body`] from a concise inline description.
735///
736/// # Syntax
737///
738/// ```text
739/// obligation_body!("scope_name", |b| {
740///     let permit = b.reserve(ObligationKind::SendPermit);
741///     b.branch(|bb| {
742///         bb.arm(|a| { a.commit(permit); });
743///         bb.arm(|a| { a.abort(permit); });
744///     });
745/// })
746/// ```
747///
748/// # Example
749///
750/// ```
751/// use asupersync::obligation_body;
752/// use asupersync::record::ObligationKind;
753///
754/// let body = obligation_body!("handler", |b| {
755///     let v = b.reserve(ObligationKind::SendPermit);
756///     b.commit(v);
757/// });
758///
759/// let mut checker = asupersync::obligation::LeakChecker::new();
760/// assert!(checker.check(&body).is_clean());
761/// ```
762#[macro_export]
763macro_rules! obligation_body {
764    ($name:expr, |$b:ident| $block:block) => {{
765        let mut $b = $crate::obligation::BodyBuilder::new($name);
766        $block
767        $b.build()
768    }};
769}
770
771/// Assert that an obligation body (or inline builder) has no leaks.
772///
773/// # Forms
774///
775/// ```text
776/// // Check an existing Body:
777/// assert_no_leaks!(body);
778///
779/// // Build and check inline:
780/// assert_no_leaks!("scope_name", |b| {
781///     let v = b.reserve(ObligationKind::SendPermit);
782///     b.commit(v);
783/// });
784/// ```
785///
786/// # Example
787///
788/// ```
789/// use asupersync::assert_no_leaks;
790/// use asupersync::record::ObligationKind;
791///
792/// // Inline form:
793/// assert_no_leaks!("clean_handler", |b| {
794///     let v = b.reserve(ObligationKind::SendPermit);
795///     b.commit(v);
796/// });
797/// ```
798///
799/// ```should_panic
800/// use asupersync::assert_no_leaks;
801/// use asupersync::record::ObligationKind;
802///
803/// // This panics because the obligation is never resolved:
804/// assert_no_leaks!("leaky_handler", |b| {
805///     let _v = b.reserve(ObligationKind::SendPermit);
806/// });
807/// ```
808#[macro_export]
809macro_rules! assert_no_leaks {
810    ($body:expr) => {{
811        let mut __checker = $crate::obligation::LeakChecker::new();
812        let __result = __checker.check(&$body);
813        assert!(
814            __result.is_clean(),
815            "obligation leak check failed:\n{__result}"
816        );
817    }};
818    ($name:expr, |$b:ident| $block:block) => {{
819        let __body = $crate::obligation_body!($name, |$b| $block);
820        $crate::assert_no_leaks!(__body);
821    }};
822}
823
824/// Assert that an obligation body has exactly the specified number of leaks.
825///
826/// # Example
827///
828/// ```
829/// use asupersync::{assert_has_leaks, obligation_body};
830/// use asupersync::record::ObligationKind;
831///
832/// let body = obligation_body!("leaky", |b| {
833///     let _v = b.reserve(ObligationKind::SendPermit);
834///     // No commit or abort — definite leak.
835/// });
836/// assert_has_leaks!(body, 1);
837/// ```
838#[macro_export]
839macro_rules! assert_has_leaks {
840    ($body:expr, $expected:expr) => {{
841        let mut __checker = $crate::obligation::LeakChecker::new();
842        let __result = __checker.check(&$body);
843        let __leaks = __result.leaks();
844        assert_eq!(
845            __leaks.len(),
846            $expected,
847            "expected {} leak(s) but found {}:\n{__result}",
848            $expected,
849            __leaks.len()
850        );
851    }};
852}
853
854// ============================================================================
855// Tests
856// ============================================================================
857
858#[cfg(test)]
859mod tests {
860    use super::*;
861
862    fn init_test(name: &str) {
863        crate::test_utils::init_test_logging();
864        crate::test_phase!(name);
865    }
866
867    fn v(n: u32) -> ObligationVar {
868        ObligationVar(n)
869    }
870
871    // ---- Clean paths -------------------------------------------------------
872
873    #[test]
874    fn clean_reserve_commit() {
875        init_test("clean_reserve_commit");
876        let body = Body::new(
877            "clean_fn",
878            vec![
879                Instruction::Reserve {
880                    var: v(0),
881                    kind: ObligationKind::SendPermit,
882                },
883                Instruction::Commit { var: v(0) },
884            ],
885        );
886
887        let mut checker = LeakChecker::new();
888        let result = checker.check(&body);
889        let is_clean = result.is_clean();
890        crate::assert_with_log!(is_clean, "clean", true, is_clean);
891        crate::test_complete!("clean_reserve_commit");
892    }
893
894    #[test]
895    fn clean_reserve_abort() {
896        init_test("clean_reserve_abort");
897        let body = Body::new(
898            "clean_abort",
899            vec![
900                Instruction::Reserve {
901                    var: v(0),
902                    kind: ObligationKind::Ack,
903                },
904                Instruction::Abort { var: v(0) },
905            ],
906        );
907
908        let mut checker = LeakChecker::new();
909        let result = checker.check(&body);
910        let is_clean = result.is_clean();
911        crate::assert_with_log!(is_clean, "clean", true, is_clean);
912        crate::test_complete!("clean_reserve_abort");
913    }
914
915    #[test]
916    fn clean_branch_both_resolve() {
917        init_test("clean_branch_both_resolve");
918        let body = Body::new(
919            "clean_branch",
920            vec![
921                Instruction::Reserve {
922                    var: v(0),
923                    kind: ObligationKind::Lease,
924                },
925                Instruction::Branch {
926                    arms: vec![
927                        vec![Instruction::Commit { var: v(0) }],
928                        vec![Instruction::Abort { var: v(0) }],
929                    ],
930                },
931            ],
932        );
933
934        let mut checker = LeakChecker::new();
935        let result = checker.check(&body);
936        let is_clean = result.is_clean();
937        crate::assert_with_log!(is_clean, "clean", true, is_clean);
938        crate::test_complete!("clean_branch_both_resolve");
939    }
940
941    #[test]
942    fn clean_multiple_obligations() {
943        init_test("clean_multiple_obligations");
944        let body = Body::new(
945            "multi_clean",
946            vec![
947                Instruction::Reserve {
948                    var: v(0),
949                    kind: ObligationKind::SendPermit,
950                },
951                Instruction::Reserve {
952                    var: v(1),
953                    kind: ObligationKind::IoOp,
954                },
955                Instruction::Commit { var: v(0) },
956                Instruction::Commit { var: v(1) },
957            ],
958        );
959
960        let mut checker = LeakChecker::new();
961        let result = checker.check(&body);
962        let is_clean = result.is_clean();
963        crate::assert_with_log!(is_clean, "clean", true, is_clean);
964        crate::test_complete!("clean_multiple_obligations");
965    }
966
967    // ---- Definite leaks ----------------------------------------------------
968
969    #[test]
970    fn definite_leak_no_resolve() {
971        init_test("definite_leak_no_resolve");
972        let body = Body::new(
973            "leaky_fn",
974            vec![Instruction::Reserve {
975                var: v(0),
976                kind: ObligationKind::SendPermit,
977            }],
978        );
979
980        let mut checker = LeakChecker::new();
981        let result = checker.check(&body);
982        let is_clean = result.is_clean();
983        crate::assert_with_log!(!is_clean, "not clean", false, is_clean);
984        let leaks = result.leaks();
985        let len = leaks.len();
986        crate::assert_with_log!(len == 1, "leak count", 1, len);
987        let kind = &leaks[0].kind;
988        crate::assert_with_log!(
989            *kind == DiagnosticKind::DefiniteLeak,
990            "kind",
991            "definite-leak",
992            kind
993        );
994        let obl_kind = leaks[0].obligation_kind;
995        crate::assert_with_log!(
996            obl_kind == Some(ObligationKind::SendPermit),
997            "obligation_kind",
998            Some(ObligationKind::SendPermit),
999            obl_kind
1000        );
1001        crate::test_complete!("definite_leak_no_resolve");
1002    }
1003
1004    #[test]
1005    fn definite_leak_multiple_vars() {
1006        init_test("definite_leak_multiple_vars");
1007        let body = Body::new(
1008            "double_leak",
1009            vec![
1010                Instruction::Reserve {
1011                    var: v(0),
1012                    kind: ObligationKind::SendPermit,
1013                },
1014                Instruction::Reserve {
1015                    var: v(1),
1016                    kind: ObligationKind::IoOp,
1017                },
1018            ],
1019        );
1020
1021        let mut checker = LeakChecker::new();
1022        let result = checker.check(&body);
1023        let leaks = result.leaks();
1024        let len = leaks.len();
1025        crate::assert_with_log!(len == 2, "leak count", 2, len);
1026        // Deterministic order: v0, v1.
1027        let var0 = leaks[0].var;
1028        crate::assert_with_log!(var0 == v(0), "var0", v(0), var0);
1029        let var1 = leaks[1].var;
1030        crate::assert_with_log!(var1 == v(1), "var1", v(1), var1);
1031        crate::test_complete!("definite_leak_multiple_vars");
1032    }
1033
1034    // ---- Potential leaks (branch-dependent) --------------------------------
1035
1036    #[test]
1037    fn potential_leak_one_arm_missing_resolve() {
1038        init_test("potential_leak_one_arm");
1039        let body = Body::new(
1040            "branch_leak",
1041            vec![
1042                Instruction::Reserve {
1043                    var: v(0),
1044                    kind: ObligationKind::Ack,
1045                },
1046                Instruction::Branch {
1047                    arms: vec![
1048                        vec![Instruction::Commit { var: v(0) }],
1049                        vec![], // No resolve in this arm.
1050                    ],
1051                },
1052            ],
1053        );
1054
1055        let mut checker = LeakChecker::new();
1056        let result = checker.check(&body);
1057        let leaks = result.leaks();
1058        let len = leaks.len();
1059        crate::assert_with_log!(len == 1, "leak count", 1, len);
1060        let kind = &leaks[0].kind;
1061        crate::assert_with_log!(
1062            *kind == DiagnosticKind::PotentialLeak,
1063            "kind",
1064            "potential-leak",
1065            kind
1066        );
1067        crate::test_complete!("potential_leak_one_arm");
1068    }
1069
1070    #[test]
1071    fn potential_leak_three_arms_one_missing() {
1072        init_test("potential_leak_three_arms");
1073        let body = Body::new(
1074            "match_leak",
1075            vec![
1076                Instruction::Reserve {
1077                    var: v(0),
1078                    kind: ObligationKind::Lease,
1079                },
1080                Instruction::Branch {
1081                    arms: vec![
1082                        vec![Instruction::Commit { var: v(0) }],
1083                        vec![Instruction::Abort { var: v(0) }],
1084                        vec![], // Missing resolve.
1085                    ],
1086                },
1087            ],
1088        );
1089
1090        let mut checker = LeakChecker::new();
1091        let result = checker.check(&body);
1092        let leaks = result.leaks();
1093        let len = leaks.len();
1094        crate::assert_with_log!(len == 1, "leak count", 1, len);
1095        let kind = &leaks[0].kind;
1096        crate::assert_with_log!(
1097            *kind == DiagnosticKind::PotentialLeak,
1098            "kind",
1099            "potential-leak",
1100            kind
1101        );
1102        crate::test_complete!("potential_leak_three_arms");
1103    }
1104
1105    // ---- Double resolve ----------------------------------------------------
1106
1107    #[test]
1108    fn double_resolve_detected() {
1109        init_test("double_resolve_detected");
1110        let body = Body::new(
1111            "double_resolve",
1112            vec![
1113                Instruction::Reserve {
1114                    var: v(0),
1115                    kind: ObligationKind::SendPermit,
1116                },
1117                Instruction::Commit { var: v(0) },
1118                Instruction::Commit { var: v(0) },
1119            ],
1120        );
1121
1122        let mut checker = LeakChecker::new();
1123        let result = checker.check(&body);
1124        let doubles = result.double_resolves();
1125        let len = doubles.len();
1126        crate::assert_with_log!(len == 1, "double count", 1, len);
1127        let leaks = result.leaks();
1128        let leak_len = leaks.len();
1129        crate::assert_with_log!(leak_len == 0, "no leaks", 0, leak_len);
1130        crate::test_complete!("double_resolve_detected");
1131    }
1132
1133    // ---- Resolve unheld ----------------------------------------------------
1134
1135    #[test]
1136    fn resolve_unheld_detected() {
1137        init_test("resolve_unheld_detected");
1138        let body = Body::new("resolve_unheld", vec![Instruction::Commit { var: v(0) }]);
1139
1140        let mut checker = LeakChecker::new();
1141        let result = checker.check(&body);
1142        let is_clean = result.is_clean();
1143        crate::assert_with_log!(!is_clean, "not clean", false, is_clean);
1144        let first_kind = &result.diagnostics[0].kind;
1145        crate::assert_with_log!(
1146            *first_kind == DiagnosticKind::ResolveUnheld,
1147            "kind",
1148            "resolve-unheld",
1149            first_kind
1150        );
1151        crate::test_complete!("resolve_unheld_detected");
1152    }
1153
1154    // ---- Overwrite leak (reserve over held) --------------------------------
1155
1156    #[test]
1157    fn overwrite_leak_detected() {
1158        init_test("overwrite_leak_detected");
1159        let body = Body::new(
1160            "overwrite",
1161            vec![
1162                Instruction::Reserve {
1163                    var: v(0),
1164                    kind: ObligationKind::SendPermit,
1165                },
1166                // Overwrite without resolving first.
1167                Instruction::Reserve {
1168                    var: v(0),
1169                    kind: ObligationKind::IoOp,
1170                },
1171                Instruction::Commit { var: v(0) },
1172            ],
1173        );
1174
1175        let mut checker = LeakChecker::new();
1176        let result = checker.check(&body);
1177        // Should detect the overwrite-leak.
1178        let leak_count = result
1179            .diagnostics
1180            .iter()
1181            .filter(|d| d.kind == DiagnosticKind::DefiniteLeak)
1182            .count();
1183        crate::assert_with_log!(leak_count == 1, "overwrite leak", 1, leak_count);
1184        // The second obligation is committed, so no exit leak.
1185        crate::test_complete!("overwrite_leak_detected");
1186    }
1187
1188    // ---- Nested branches ---------------------------------------------------
1189
1190    #[test]
1191    fn nested_branch_clean() {
1192        init_test("nested_branch_clean");
1193        let body = Body::new(
1194            "nested_clean",
1195            vec![
1196                Instruction::Reserve {
1197                    var: v(0),
1198                    kind: ObligationKind::SendPermit,
1199                },
1200                Instruction::Branch {
1201                    arms: vec![
1202                        vec![Instruction::Branch {
1203                            arms: vec![
1204                                vec![Instruction::Commit { var: v(0) }],
1205                                vec![Instruction::Abort { var: v(0) }],
1206                            ],
1207                        }],
1208                        vec![Instruction::Abort { var: v(0) }],
1209                    ],
1210                },
1211            ],
1212        );
1213
1214        let mut checker = LeakChecker::new();
1215        let result = checker.check(&body);
1216        let is_clean = result.is_clean();
1217        crate::assert_with_log!(is_clean, "clean", true, is_clean);
1218        crate::test_complete!("nested_branch_clean");
1219    }
1220
1221    #[test]
1222    fn nested_branch_leak() {
1223        init_test("nested_branch_leak");
1224        let body = Body::new(
1225            "nested_leak",
1226            vec![
1227                Instruction::Reserve {
1228                    var: v(0),
1229                    kind: ObligationKind::Lease,
1230                },
1231                Instruction::Branch {
1232                    arms: vec![
1233                        vec![Instruction::Branch {
1234                            arms: vec![
1235                                vec![Instruction::Commit { var: v(0) }],
1236                                vec![], // Nested leak path.
1237                            ],
1238                        }],
1239                        vec![Instruction::Abort { var: v(0) }],
1240                    ],
1241                },
1242            ],
1243        );
1244
1245        let mut checker = LeakChecker::new();
1246        let result = checker.check(&body);
1247        let leaks = result.leaks();
1248        let len = leaks.len();
1249        crate::assert_with_log!(len == 1, "leak count", 1, len);
1250        let kind = &leaks[0].kind;
1251        crate::assert_with_log!(
1252            *kind == DiagnosticKind::PotentialLeak,
1253            "kind",
1254            "potential-leak",
1255            kind
1256        );
1257        crate::test_complete!("nested_branch_leak");
1258    }
1259
1260    // ---- Realistic: channel send permit pattern ----------------------------
1261
1262    #[test]
1263    fn realistic_channel_send_permit() {
1264        init_test("realistic_channel_send_permit");
1265        // Models the two-phase send pattern:
1266        //   let permit = channel.reserve_send();  // Reserve
1267        //   if condition {
1268        //     permit.send(data);                  // Commit
1269        //   } else {
1270        //     permit.cancel();                    // Abort
1271        //   }
1272        let body = Body::new(
1273            "channel_send",
1274            vec![
1275                Instruction::Reserve {
1276                    var: v(0),
1277                    kind: ObligationKind::SendPermit,
1278                },
1279                Instruction::Branch {
1280                    arms: vec![
1281                        vec![Instruction::Commit { var: v(0) }],
1282                        vec![Instruction::Abort { var: v(0) }],
1283                    ],
1284                },
1285            ],
1286        );
1287
1288        let mut checker = LeakChecker::new();
1289        let result = checker.check(&body);
1290        let is_clean = result.is_clean();
1291        crate::assert_with_log!(is_clean, "clean send permit", true, is_clean);
1292        crate::test_complete!("realistic_channel_send_permit");
1293    }
1294
1295    #[test]
1296    fn realistic_leaky_send_permit() {
1297        init_test("realistic_leaky_send_permit");
1298        // Models a buggy pattern where the error path forgets to cancel:
1299        //   let permit = channel.reserve_send();
1300        //   if ok {
1301        //     permit.send(data);
1302        //   } else {
1303        //     log_error();  // BUG: forgot to cancel permit
1304        //   }
1305        let body = Body::new(
1306            "leaky_send",
1307            vec![
1308                Instruction::Reserve {
1309                    var: v(0),
1310                    kind: ObligationKind::SendPermit,
1311                },
1312                Instruction::Branch {
1313                    arms: vec![
1314                        vec![Instruction::Commit { var: v(0) }],
1315                        vec![], // Bug: no cancel on error path.
1316                    ],
1317                },
1318            ],
1319        );
1320
1321        let mut checker = LeakChecker::new();
1322        let result = checker.check(&body);
1323        let leaks = result.leaks();
1324        let len = leaks.len();
1325        crate::assert_with_log!(len == 1, "leak count", 1, len);
1326        let kind = &leaks[0].kind;
1327        crate::assert_with_log!(
1328            *kind == DiagnosticKind::PotentialLeak,
1329            "kind",
1330            "potential-leak",
1331            kind
1332        );
1333        tracing::debug!(result = %result, "leak checker result");
1334        crate::test_complete!("realistic_leaky_send_permit");
1335    }
1336
1337    // ---- Realistic: I/O operation with timeout and cancel ------------------
1338
1339    #[test]
1340    fn realistic_io_with_timeout() {
1341        init_test("realistic_io_with_timeout");
1342        // Models:
1343        //   let io = reserve_io();
1344        //   match race(io_complete, timeout) {
1345        //     IoComplete => io.commit(),
1346        //     Timeout => io.abort(),
1347        //     Cancel => io.abort(),
1348        //   }
1349        let body = Body::new(
1350            "io_timeout",
1351            vec![
1352                Instruction::Reserve {
1353                    var: v(0),
1354                    kind: ObligationKind::IoOp,
1355                },
1356                Instruction::Branch {
1357                    arms: vec![
1358                        vec![Instruction::Commit { var: v(0) }],
1359                        vec![Instruction::Abort { var: v(0) }],
1360                        vec![Instruction::Abort { var: v(0) }],
1361                    ],
1362                },
1363            ],
1364        );
1365
1366        let mut checker = LeakChecker::new();
1367        let result = checker.check(&body);
1368        let is_clean = result.is_clean();
1369        crate::assert_with_log!(is_clean, "clean", true, is_clean);
1370        crate::test_complete!("realistic_io_with_timeout");
1371    }
1372
1373    // ---- Realistic: lease with nested region close -------------------------
1374
1375    #[test]
1376    fn realistic_lease_pattern() {
1377        init_test("realistic_lease_pattern");
1378        // Models a lease pattern with multiple obligations:
1379        //   let lease = acquire_lease();         // v0: Lease
1380        //   let ack = receive_message();          // v1: Ack
1381        //   process(data);
1382        //   ack.acknowledge();                    // commit v1
1383        //   lease.release();                      // commit v0
1384        let body = Body::new(
1385            "lease_and_ack",
1386            vec![
1387                Instruction::Reserve {
1388                    var: v(0),
1389                    kind: ObligationKind::Lease,
1390                },
1391                Instruction::Reserve {
1392                    var: v(1),
1393                    kind: ObligationKind::Ack,
1394                },
1395                Instruction::Commit { var: v(1) },
1396                Instruction::Commit { var: v(0) },
1397            ],
1398        );
1399
1400        let mut checker = LeakChecker::new();
1401        let result = checker.check(&body);
1402        let is_clean = result.is_clean();
1403        crate::assert_with_log!(is_clean, "clean", true, is_clean);
1404        crate::test_complete!("realistic_lease_pattern");
1405    }
1406
1407    #[test]
1408    fn realistic_lease_leak_on_error() {
1409        init_test("realistic_lease_leak_on_error");
1410        // Models a buggy lease pattern: error during processing leaks the lease.
1411        //   let lease = acquire_lease();         // v0: Lease
1412        //   let ack = receive_message();          // v1: Ack
1413        //   if error {
1414        //     ack.reject();                       // abort v1
1415        //     // BUG: forgot to release lease
1416        //   } else {
1417        //     process(data);
1418        //     ack.acknowledge();                  // commit v1
1419        //     lease.release();                    // commit v0
1420        //   }
1421        let body = Body::new(
1422            "lease_error_leak",
1423            vec![
1424                Instruction::Reserve {
1425                    var: v(0),
1426                    kind: ObligationKind::Lease,
1427                },
1428                Instruction::Reserve {
1429                    var: v(1),
1430                    kind: ObligationKind::Ack,
1431                },
1432                Instruction::Branch {
1433                    arms: vec![
1434                        vec![
1435                            Instruction::Abort { var: v(1) },
1436                            // BUG: v0 not resolved.
1437                        ],
1438                        vec![
1439                            Instruction::Commit { var: v(1) },
1440                            Instruction::Commit { var: v(0) },
1441                        ],
1442                    ],
1443                },
1444            ],
1445        );
1446
1447        let mut checker = LeakChecker::new();
1448        let result = checker.check(&body);
1449        let leaks = result.leaks();
1450        let len = leaks.len();
1451        crate::assert_with_log!(len == 1, "leak count", 1, len);
1452        let leaked_var = leaks[0].var;
1453        crate::assert_with_log!(leaked_var == v(0), "leaked var", v(0), leaked_var);
1454        let leaked_kind = leaks[0].obligation_kind;
1455        crate::assert_with_log!(
1456            leaked_kind == Some(ObligationKind::Lease),
1457            "leaked kind",
1458            Some(ObligationKind::Lease),
1459            leaked_kind
1460        );
1461        tracing::debug!(result = %result, "leak checker result");
1462        crate::test_complete!("realistic_lease_leak_on_error");
1463    }
1464
1465    // ---- Checker reuse -----------------------------------------------------
1466
1467    #[test]
1468    fn checker_reuse_across_bodies() {
1469        init_test("checker_reuse_across_bodies");
1470        let mut checker = LeakChecker::new();
1471
1472        let clean_body = Body::new(
1473            "clean",
1474            vec![
1475                Instruction::Reserve {
1476                    var: v(0),
1477                    kind: ObligationKind::SendPermit,
1478                },
1479                Instruction::Commit { var: v(0) },
1480            ],
1481        );
1482        let r1 = checker.check(&clean_body);
1483        let is_clean = r1.is_clean();
1484        crate::assert_with_log!(is_clean, "first clean", true, is_clean);
1485
1486        let leaky_body = Body::new(
1487            "leaky",
1488            vec![Instruction::Reserve {
1489                var: v(0),
1490                kind: ObligationKind::Lease,
1491            }],
1492        );
1493        let r2 = checker.check(&leaky_body);
1494        let is_clean2 = r2.is_clean();
1495        crate::assert_with_log!(!is_clean2, "second leaky", false, is_clean2);
1496
1497        // Check that first result was not contaminated.
1498        let first_leaks = r1.leaks().len();
1499        crate::assert_with_log!(first_leaks == 0, "first still clean", 0, first_leaks);
1500        crate::test_complete!("checker_reuse_across_bodies");
1501    }
1502
1503    // ---- Deterministic output ----------------------------------------------
1504
1505    #[test]
1506    fn deterministic_diagnostic_order() {
1507        init_test("deterministic_diagnostic_order");
1508        // Multiple leaks should be reported in variable-index order.
1509        let body = Body::new(
1510            "multi_leak",
1511            vec![
1512                Instruction::Reserve {
1513                    var: v(2),
1514                    kind: ObligationKind::IoOp,
1515                },
1516                Instruction::Reserve {
1517                    var: v(0),
1518                    kind: ObligationKind::SendPermit,
1519                },
1520                Instruction::Reserve {
1521                    var: v(1),
1522                    kind: ObligationKind::Lease,
1523                },
1524            ],
1525        );
1526
1527        let mut checker = LeakChecker::new();
1528        let result = checker.check(&body);
1529        let leaks = result.leaks();
1530        let len = leaks.len();
1531        crate::assert_with_log!(len == 3, "leak count", 3, len);
1532        let vars: Vec<u32> = leaks.iter().map(|d| d.var.0).collect();
1533        crate::assert_with_log!(vars == vec![0, 1, 2], "order", vec![0u32, 1, 2], vars);
1534        crate::test_complete!("deterministic_diagnostic_order");
1535    }
1536
1537    // ---- Display impls -----------------------------------------------------
1538
1539    #[test]
1540    fn display_impls() {
1541        init_test("display_impls");
1542        let var = ObligationVar(42);
1543        let var_str = format!("{var}");
1544        crate::assert_with_log!(var_str == "v42", "var display", "v42", var_str);
1545
1546        let state = VarState::Held(ObligationKind::SendPermit);
1547        let state_str = format!("{state}");
1548        crate::assert_with_log!(
1549            state_str == "held(send_permit)",
1550            "state display",
1551            "held(send_permit)",
1552            state_str
1553        );
1554
1555        let diag = Diagnostic {
1556            kind: DiagnosticKind::DefiniteLeak,
1557            var: ObligationVar(0),
1558            obligation_kind: Some(ObligationKind::SendPermit),
1559            scope: "test_fn".to_string(),
1560            message: "v0 leaked".to_string(),
1561        };
1562        let diag_str = format!("{diag}");
1563        let has_fn = diag_str.contains("test_fn");
1564        crate::assert_with_log!(has_fn, "diag has scope", true, has_fn);
1565        let has_kind = diag_str.contains("definite-leak");
1566        crate::assert_with_log!(has_kind, "diag has kind", true, has_kind);
1567        crate::test_complete!("display_impls");
1568    }
1569
1570    // ---- VarState lattice join exhaustive ----------------------------------
1571
1572    #[test]
1573    fn var_state_join_lattice() {
1574        init_test("var_state_join_lattice");
1575        let k = ObligationKind::SendPermit;
1576        let k2 = ObligationKind::IoOp;
1577
1578        // Identity.
1579        let r = VarState::Empty.join(VarState::Empty);
1580        crate::assert_with_log!(r == VarState::Empty, "e+e", VarState::Empty, r);
1581        let r = VarState::Resolved.join(VarState::Resolved);
1582        crate::assert_with_log!(r == VarState::Resolved, "r+r", VarState::Resolved, r);
1583        let r = VarState::Held(k).join(VarState::Held(k));
1584        crate::assert_with_log!(r == VarState::Held(k), "h+h", VarState::Held(k), r);
1585
1586        // Held + Resolved => MayHold.
1587        let r = VarState::Held(k).join(VarState::Resolved);
1588        crate::assert_with_log!(r == VarState::MayHold(k), "h+r", VarState::MayHold(k), r);
1589
1590        // Held + Empty => MayHold.
1591        let r = VarState::Held(k).join(VarState::Empty);
1592        crate::assert_with_log!(r == VarState::MayHold(k), "h+e", VarState::MayHold(k), r);
1593
1594        // Resolved + Empty => Resolved.
1595        let r = VarState::Resolved.join(VarState::Empty);
1596        crate::assert_with_log!(r == VarState::Resolved, "r+e", VarState::Resolved, r);
1597
1598        // MayHold propagates.
1599        let r = VarState::MayHold(k).join(VarState::Empty);
1600        crate::assert_with_log!(r == VarState::MayHold(k), "m+e", VarState::MayHold(k), r);
1601        let r = VarState::MayHold(k).join(VarState::Resolved);
1602        crate::assert_with_log!(r == VarState::MayHold(k), "m+r", VarState::MayHold(k), r);
1603
1604        // Different kinds => MayHoldAmbiguous.
1605        let r = VarState::Held(k).join(VarState::Held(k2));
1606        crate::assert_with_log!(
1607            r == VarState::MayHoldAmbiguous,
1608            "h(k1)+h(k2)",
1609            VarState::MayHoldAmbiguous,
1610            r
1611        );
1612
1613        // Commutativity check.
1614        let r1 = VarState::Held(k).join(VarState::Resolved);
1615        let r2 = VarState::Resolved.join(VarState::Held(k));
1616        crate::assert_with_log!(r1 == r2, "commutative", r1, r2);
1617
1618        crate::test_complete!("var_state_join_lattice");
1619    }
1620
1621    // ---- Empty body --------------------------------------------------------
1622
1623    #[test]
1624    fn empty_body_is_clean() {
1625        init_test("empty_body_is_clean");
1626        let body = Body::new("empty", vec![]);
1627        let mut checker = LeakChecker::new();
1628        let result = checker.check(&body);
1629        let is_clean = result.is_clean();
1630        crate::assert_with_log!(is_clean, "clean", true, is_clean);
1631        crate::test_complete!("empty_body_is_clean");
1632    }
1633
1634    // ---- CheckResult display -----------------------------------------------
1635
1636    #[test]
1637    fn check_result_display() {
1638        init_test("check_result_display");
1639        let clean = CheckResult {
1640            scope: "clean_fn".to_string(),
1641            diagnostics: vec![],
1642        };
1643        let clean_str = format!("{clean}");
1644        let has_no_issues = clean_str.contains("no issues");
1645        crate::assert_with_log!(has_no_issues, "clean display", true, has_no_issues);
1646
1647        let dirty = CheckResult {
1648            scope: "dirty_fn".to_string(),
1649            diagnostics: vec![Diagnostic {
1650                kind: DiagnosticKind::DefiniteLeak,
1651                var: v(0),
1652                obligation_kind: Some(ObligationKind::SendPermit),
1653                scope: "dirty_fn".to_string(),
1654                message: "test".to_string(),
1655            }],
1656        };
1657        let dirty_str = format!("{dirty}");
1658        let has_count = dirty_str.contains("1 diagnostic");
1659        crate::assert_with_log!(has_count, "dirty display", true, has_count);
1660        crate::test_complete!("check_result_display");
1661    }
1662
1663    // ---- BodyBuilder -------------------------------------------------------
1664
1665    #[test]
1666    fn builder_clean_reserve_commit() {
1667        init_test("builder_clean_reserve_commit");
1668        let mut b = BodyBuilder::new("clean");
1669        let v = b.reserve(ObligationKind::SendPermit);
1670        b.commit(v);
1671        let body = b.build();
1672        let mut checker = LeakChecker::new();
1673        let result = checker.check(&body);
1674        crate::assert_with_log!(result.is_clean(), "clean", true, result.is_clean());
1675        crate::test_complete!("builder_clean_reserve_commit");
1676    }
1677
1678    #[test]
1679    fn builder_leak_detected() {
1680        init_test("builder_leak_detected");
1681        let mut b = BodyBuilder::new("leaky");
1682        let _v = b.reserve(ObligationKind::Lease);
1683        let body = b.build();
1684        let mut checker = LeakChecker::new();
1685        let result = checker.check(&body);
1686        let leaks = result.leaks();
1687        crate::assert_with_log!(leaks.len() == 1, "leak count", 1, leaks.len());
1688        crate::test_complete!("builder_leak_detected");
1689    }
1690
1691    #[test]
1692    fn builder_branch_clean() {
1693        init_test("builder_branch_clean");
1694        let mut b = BodyBuilder::new("branch_clean");
1695        let v = b.reserve(ObligationKind::IoOp);
1696        b.branch(|bb| {
1697            bb.arm(|a| {
1698                a.commit(v);
1699            });
1700            bb.arm(|a| {
1701                a.abort(v);
1702            });
1703        });
1704        let body = b.build();
1705        let mut checker = LeakChecker::new();
1706        let result = checker.check(&body);
1707        crate::assert_with_log!(result.is_clean(), "clean", true, result.is_clean());
1708        crate::test_complete!("builder_branch_clean");
1709    }
1710
1711    #[test]
1712    fn builder_branch_potential_leak() {
1713        init_test("builder_branch_potential_leak");
1714        let mut b = BodyBuilder::new("branch_leak");
1715        let v = b.reserve(ObligationKind::Ack);
1716        b.branch(|bb| {
1717            bb.arm(|a| {
1718                a.commit(v);
1719            });
1720            bb.arm(|_a| {}); // Missing resolution.
1721        });
1722        let body = b.build();
1723        let mut checker = LeakChecker::new();
1724        let result = checker.check(&body);
1725        let leaks = result.leaks();
1726        crate::assert_with_log!(leaks.len() == 1, "leak count", 1, leaks.len());
1727        let kind = &leaks[0].kind;
1728        crate::assert_with_log!(
1729            *kind == DiagnosticKind::PotentialLeak,
1730            "potential",
1731            DiagnosticKind::PotentialLeak,
1732            kind
1733        );
1734        crate::test_complete!("builder_branch_potential_leak");
1735    }
1736
1737    #[test]
1738    fn builder_auto_var_numbering() {
1739        init_test("builder_auto_var_numbering");
1740        let mut b = BodyBuilder::new("auto_vars");
1741        let v0 = b.reserve(ObligationKind::SendPermit);
1742        let v1 = b.reserve(ObligationKind::Ack);
1743        let v2 = b.reserve(ObligationKind::Lease);
1744        crate::assert_with_log!(v0 == ObligationVar(0), "v0", ObligationVar(0), v0);
1745        crate::assert_with_log!(v1 == ObligationVar(1), "v1", ObligationVar(1), v1);
1746        crate::assert_with_log!(v2 == ObligationVar(2), "v2", ObligationVar(2), v2);
1747        b.commit(v0);
1748        b.commit(v1);
1749        b.commit(v2);
1750        let body = b.build();
1751        let mut checker = LeakChecker::new();
1752        crate::assert_with_log!(
1753            checker.check(&body).is_clean(),
1754            "clean",
1755            true,
1756            checker.check(&body).is_clean()
1757        );
1758        crate::test_complete!("builder_auto_var_numbering");
1759    }
1760
1761    #[test]
1762    fn builder_nested_branch() {
1763        init_test("builder_nested_branch");
1764        let mut b = BodyBuilder::new("nested");
1765        let v = b.reserve(ObligationKind::SendPermit);
1766        b.branch(|bb| {
1767            bb.arm(|a| {
1768                a.branch(|bb2| {
1769                    bb2.arm(|a2| {
1770                        a2.commit(v);
1771                    });
1772                    bb2.arm(|a2| {
1773                        a2.abort(v);
1774                    });
1775                });
1776            });
1777            bb.arm(|a| {
1778                a.abort(v);
1779            });
1780        });
1781        let body = b.build();
1782        let mut checker = LeakChecker::new();
1783        crate::assert_with_log!(
1784            checker.check(&body).is_clean(),
1785            "clean",
1786            true,
1787            checker.check(&body).is_clean()
1788        );
1789        crate::test_complete!("builder_nested_branch");
1790    }
1791
1792    // ---- ObligationAnalyzer ------------------------------------------------
1793
1794    #[test]
1795    fn analyzer_clean() {
1796        init_test("analyzer_clean");
1797        let mut a = ObligationAnalyzer::new("clean_scope");
1798        let v = a.reserve(ObligationKind::SendPermit);
1799        a.commit(v);
1800        a.assert_clean();
1801        crate::test_complete!("analyzer_clean");
1802    }
1803
1804    #[test]
1805    fn analyzer_detects_leak() {
1806        init_test("analyzer_detects_leak");
1807        let mut a = ObligationAnalyzer::new("leaky_scope");
1808        let _v = a.reserve(ObligationKind::Lease);
1809        a.assert_leaks(1);
1810        crate::test_complete!("analyzer_detects_leak");
1811    }
1812
1813    #[test]
1814    fn analyzer_branch_clean() {
1815        init_test("analyzer_branch_clean");
1816        let mut a = ObligationAnalyzer::new("branch_scope");
1817        let v = a.reserve(ObligationKind::IoOp);
1818        a.branch(|bb| {
1819            bb.arm(|arm| {
1820                arm.commit(v);
1821            });
1822            bb.arm(|arm| {
1823                arm.abort(v);
1824            });
1825        });
1826        a.assert_clean();
1827        crate::test_complete!("analyzer_branch_clean");
1828    }
1829
1830    #[test]
1831    fn analyzer_branch_leak() {
1832        init_test("analyzer_branch_leak");
1833        let mut a = ObligationAnalyzer::new("branch_leak");
1834        let v = a.reserve(ObligationKind::Ack);
1835        a.branch(|bb| {
1836            bb.arm(|arm| {
1837                arm.commit(v);
1838            });
1839            bb.arm(|_arm| {}); // Missing.
1840        });
1841        a.assert_leaks(1);
1842        crate::test_complete!("analyzer_branch_leak");
1843    }
1844
1845    // ---- Macros ------------------------------------------------------------
1846
1847    #[test]
1848    fn macro_obligation_body_clean() {
1849        init_test("macro_obligation_body_clean");
1850        let body = crate::obligation_body!("macro_clean", |b| {
1851            let v = b.reserve(ObligationKind::SendPermit);
1852            b.commit(v);
1853        });
1854        let mut checker = LeakChecker::new();
1855        let result = checker.check(&body);
1856        crate::assert_with_log!(result.is_clean(), "clean", true, result.is_clean());
1857        crate::test_complete!("macro_obligation_body_clean");
1858    }
1859
1860    #[test]
1861    fn macro_assert_no_leaks_inline() {
1862        init_test("macro_assert_no_leaks_inline");
1863        crate::assert_no_leaks!("inline_clean", |b| {
1864            let v = b.reserve(ObligationKind::SendPermit);
1865            b.commit(v);
1866        });
1867        crate::test_complete!("macro_assert_no_leaks_inline");
1868    }
1869
1870    #[test]
1871    fn macro_assert_has_leaks() {
1872        init_test("macro_assert_has_leaks");
1873        let body = crate::obligation_body!("leaky_macro", |b| {
1874            let _v = b.reserve(ObligationKind::Lease);
1875        });
1876        crate::assert_has_leaks!(body, 1);
1877        crate::test_complete!("macro_assert_has_leaks");
1878    }
1879
1880    #[test]
1881    fn macro_assert_no_leaks_body() {
1882        init_test("macro_assert_no_leaks_body");
1883        let body = crate::obligation_body!("body_clean", |b| {
1884            let v = b.reserve(ObligationKind::Ack);
1885            b.abort(v);
1886        });
1887        crate::assert_no_leaks!(body);
1888        crate::test_complete!("macro_assert_no_leaks_body");
1889    }
1890}