1use crate::record::ObligationKind;
7use std::collections::{HashMap, HashSet};
8use std::fmt;
9
10#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum VarState {
42 Empty,
44 Held(ObligationKind),
46 MayHold(ObligationKind),
48 MayHoldAmbiguous,
50 Resolved,
52}
53
54impl VarState {
55 #[must_use]
59 pub fn join(self, other: Self) -> Self {
60 use VarState::{Empty, Held, MayHold, MayHoldAmbiguous, Resolved};
61 match (self, other) {
62 (Empty, Empty) => Empty,
64 (Resolved | Empty, Resolved) | (Resolved, Empty) => Resolved,
65
66 (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(k) | MayHold(k), Resolved | Empty) | (Resolved | Empty, Held(k) | MayHold(k)) => {
73 MayHold(k)
74 }
75
76 (MayHoldAmbiguous, _)
78 | (_, MayHoldAmbiguous)
79 | (Held(_) | MayHold(_), Held(_) | MayHold(_)) => MayHoldAmbiguous,
80 }
81 }
82
83 #[must_use]
85 pub fn is_leak(&self) -> bool {
86 matches!(
87 self,
88 Self::Held(_) | Self::MayHold(_) | Self::MayHoldAmbiguous
89 )
90 }
91
92 #[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#[derive(Debug, Clone)]
123pub enum Instruction {
124 Reserve {
126 var: ObligationVar,
128 kind: ObligationKind,
130 },
131 Commit {
133 var: ObligationVar,
135 },
136 Abort {
138 var: ObligationVar,
140 },
141 Branch {
144 arms: Vec<Vec<Self>>,
146 },
147}
148
149#[derive(Debug, Clone)]
157pub struct Body {
158 pub name: String,
160 pub instructions: Vec<Instruction>,
162}
163
164impl Body {
165 #[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#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum DiagnosticKind {
182 DefiniteLeak,
184 PotentialLeak,
186 DoubleResolve,
188 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#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct Diagnostic {
206 pub kind: DiagnosticKind,
208 pub var: ObligationVar,
210 pub obligation_kind: Option<ObligationKind>,
212 pub scope: String,
214 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#[derive(Debug, Clone)]
234pub struct CheckResult {
235 pub scope: String,
237 pub diagnostics: Vec<Diagnostic>,
239}
240
241impl CheckResult {
242 #[must_use]
244 pub fn is_clean(&self) -> bool {
245 self.diagnostics.is_empty()
246 }
247
248 #[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 #[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#[derive(Debug, Default)]
301pub struct LeakChecker {
302 state: HashMap<ObligationVar, VarState>,
304 diagnostics: Vec<Diagnostic>,
306 scope_name: String,
308}
309
310impl LeakChecker {
311 #[must_use]
313 pub fn new() -> Self {
314 Self::default()
315 }
316
317 #[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 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 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 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 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 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#[derive(Debug)]
521pub struct BodyBuilder {
522 name: String,
523 instructions: Vec<Instruction>,
524 next_var: u32,
525}
526
527impl BodyBuilder {
528 #[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 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 pub fn commit(&mut self, var: ObligationVar) -> &mut Self {
548 self.instructions.push(Instruction::Commit { var });
549 self
550 }
551
552 pub fn abort(&mut self, var: ObligationVar) -> &mut Self {
554 self.instructions.push(Instruction::Abort { var });
555 self
556 }
557
558 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 #[must_use]
571 pub fn build(self) -> Body {
572 Body {
573 name: self.name,
574 instructions: self.instructions,
575 }
576 }
577
578 #[must_use]
580 pub fn next_var_index(&self) -> u32 {
581 self.next_var
582 }
583}
584
585#[derive(Debug)]
587pub struct BranchBuilder {
588 arms: Vec<Vec<Instruction>>,
589}
590
591impl BranchBuilder {
592 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#[derive(Debug)]
606pub struct ArmBuilder {
607 instructions: Vec<Instruction>,
608}
609
610impl ArmBuilder {
611 pub fn commit(&mut self, var: ObligationVar) -> &mut Self {
613 self.instructions.push(Instruction::Commit { var });
614 self
615 }
616
617 pub fn abort(&mut self, var: ObligationVar) -> &mut Self {
619 self.instructions.push(Instruction::Abort { var });
620 self
621 }
622
623 pub fn reserve(&mut self, var: ObligationVar, kind: ObligationKind) -> &mut Self {
625 self.instructions.push(Instruction::Reserve { var, kind });
626 self
627 }
628
629 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#[derive(Debug)]
662pub struct ObligationAnalyzer {
663 builder: BodyBuilder,
664}
665
666impl ObligationAnalyzer {
667 #[must_use]
669 pub fn new(scope: impl Into<String>) -> Self {
670 Self {
671 builder: BodyBuilder::new(scope),
672 }
673 }
674
675 pub fn reserve(&mut self, kind: ObligationKind) -> ObligationVar {
677 self.builder.reserve(kind)
678 }
679
680 pub fn commit(&mut self, var: ObligationVar) {
682 self.builder.commit(var);
683 }
684
685 pub fn abort(&mut self, var: ObligationVar) {
687 self.builder.abort(var);
688 }
689
690 pub fn branch(&mut self, build: impl FnOnce(&mut BranchBuilder)) {
692 self.builder.branch(build);
693 }
694
695 #[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 pub fn assert_clean(self) {
709 let result = self.check();
710 assert!(result.is_clean(), "obligation leak check failed:\n{result}");
711 }
712
713 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#[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#[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#[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#[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 #[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 #[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 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 #[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![], ],
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![], ],
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 #[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 #[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 #[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 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 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 crate::test_complete!("overwrite_leak_detected");
1186 }
1187
1188 #[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![], ],
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 #[test]
1263 fn realistic_channel_send_permit() {
1264 init_test("realistic_channel_send_permit");
1265 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 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![], ],
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 #[test]
1340 fn realistic_io_with_timeout() {
1341 init_test("realistic_io_with_timeout");
1342 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 #[test]
1376 fn realistic_lease_pattern() {
1377 init_test("realistic_lease_pattern");
1378 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 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 ],
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 #[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 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 #[test]
1506 fn deterministic_diagnostic_order() {
1507 init_test("deterministic_diagnostic_order");
1508 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 #[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 #[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 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 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 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 let r = VarState::Resolved.join(VarState::Empty);
1596 crate::assert_with_log!(r == VarState::Resolved, "r+e", VarState::Resolved, r);
1597
1598 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 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 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 #[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 #[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 #[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| {}); });
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 #[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| {}); });
1841 a.assert_leaks(1);
1842 crate::test_complete!("analyzer_branch_leak");
1843 }
1844
1845 #[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}