1use rustc_hash::{FxHashMap, FxHashSet};
23use smol_str::SmolStr;
24
25use gdscript_base::TextRange;
26
27use crate::body::{BinOp, Body, Expr, ExprId, Literal, Stmt, StmtId, UnOp};
28use crate::cst::AstPtr;
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub enum Place {
36 Local(SmolStr),
38 SelfMember(SmolStr),
40 Field(Box<Place>, SmolStr),
42}
43
44impl Place {
45 #[must_use]
47 pub fn of(body: &Body, id: ExprId) -> Option<Place> {
48 match body.expr(id) {
49 Expr::Name(n) => Some(Place::Local(n.clone())),
50 Expr::Paren(inner) => Place::of(body, *inner),
51 Expr::Field { receiver, name, .. } => match body.expr(*receiver) {
52 Expr::SelfExpr => Some(Place::SelfMember(name.clone())),
53 _ => Some(Place::Field(
54 Box::new(Place::of(body, *receiver)?),
55 name.clone(),
56 )),
57 },
58 _ => None,
60 }
61 }
62
63 #[must_use]
67 pub fn invalidated_by(&self, assigned: &Place) -> bool {
68 let mut cur = self;
69 loop {
70 if cur == assigned {
71 return true;
72 }
73 match cur {
74 Place::Field(base, _) => cur = base,
75 _ => return false,
76 }
77 }
78 }
79
80 #[must_use]
83 pub fn dotted_key(&self) -> String {
84 match self {
85 Place::Local(n) => n.to_string(),
86 Place::SelfMember(m) => format!("self.{m}"),
87 Place::Field(base, name) => format!("{}.{name}", base.dotted_key()),
88 }
89 }
90
91 #[must_use]
94 fn is_self_rooted(&self) -> bool {
95 match self {
96 Place::SelfMember(_) => true,
97 Place::Field(base, _) => base.is_self_rooted(),
98 Place::Local(_) => false,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum NarrowedTy {
108 Is(AstPtr),
110 NotNull,
113 Not(AstPtr),
116}
117
118#[derive(Debug, Clone, Default, PartialEq, Eq)]
120pub struct FlowFacts(FxHashMap<Place, NarrowedTy>);
121
122impl FlowFacts {
123 #[must_use]
125 pub fn get(&self, place: &Place) -> Option<&NarrowedTy> {
126 self.0.get(place)
127 }
128
129 #[must_use]
131 pub fn is_empty(&self) -> bool {
132 self.0.is_empty()
133 }
134
135 pub fn iter(&self) -> impl Iterator<Item = (&Place, &NarrowedTy)> {
137 self.0.iter()
138 }
139
140 fn insert(&mut self, place: Place, ty: NarrowedTy) {
143 if matches!(ty, NarrowedTy::NotNull)
144 && matches!(self.0.get(&place), Some(NarrowedTy::Is(_)))
145 {
146 return;
147 }
148 self.0.insert(place, ty);
149 }
150
151 fn invalidate_assigned(&mut self, assigned: &Place) {
153 self.0.retain(|p, _| !p.invalidated_by(assigned));
154 }
155
156 fn invalidate_self_rooted(&mut self) {
158 self.0.retain(|p, _| !p.is_self_rooted());
159 }
160
161 #[must_use]
164 fn join(&self, other: &FlowFacts) -> FlowFacts {
165 let mut out = FxHashMap::default();
166 for (p, t) in &self.0 {
167 if other.0.get(p) == Some(t) {
168 out.insert(p.clone(), t.clone());
169 }
170 }
171 FlowFacts(out)
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum UnreachableCause {
181 AfterReturn,
183 Other,
185}
186
187#[derive(Debug, Clone, Default)]
189pub struct FlowAnalysis {
190 entry_facts: FxHashMap<StmtId, FlowFacts>,
193 unreachable_anchors: Vec<(StmtId, UnreachableCause)>,
196 unreachable_pattern_anchors: Vec<TextRange>,
199}
200
201impl FlowAnalysis {
202 #[must_use]
204 pub fn facts_before(&self, stmt: StmtId) -> Option<&FlowFacts> {
205 self.entry_facts.get(&stmt)
206 }
207
208 #[must_use]
211 pub fn unreachable_ranges(&self, body: &Body) -> Vec<(TextRange, UnreachableCause)> {
212 self.unreachable_anchors
213 .iter()
214 .map(|&(sid, cause)| (body.source_map.stmt_range(sid), cause))
215 .collect()
216 }
217
218 #[must_use]
220 pub fn unreachable_pattern_ranges(&self) -> &[TextRange] {
221 &self.unreachable_pattern_anchors
222 }
223}
224
225#[must_use]
227pub fn analyze(body: &Body) -> FlowAnalysis {
228 let mut a = Analyzer {
229 body,
230 entry_facts: FxHashMap::default(),
231 unreachable_anchors: Vec::new(),
232 unreachable_pattern_anchors: Vec::new(),
233 };
234 a.block(FlowFacts::default(), &body.block);
235 for expr in &body.exprs {
239 if let Expr::Lambda { body: lbody, .. } = expr {
240 a.block(FlowFacts::default(), lbody);
241 }
242 }
243 FlowAnalysis {
244 entry_facts: a.entry_facts,
245 unreachable_anchors: a.unreachable_anchors,
246 unreachable_pattern_anchors: a.unreachable_pattern_anchors,
247 }
248}
249
250struct Analyzer<'a> {
251 body: &'a Body,
252 entry_facts: FxHashMap<StmtId, FlowFacts>,
253 unreachable_anchors: Vec<(StmtId, UnreachableCause)>,
254 unreachable_pattern_anchors: Vec<TextRange>,
255}
256
257impl Analyzer<'_> {
258 fn block(&mut self, facts: FlowFacts, block: &[StmtId]) -> Option<FlowFacts> {
262 let mut cur = Some(facts);
263 let mut prev: Option<StmtId> = None;
264 for &sid in block {
265 let Some(f) = cur else {
266 let cause = if prev.is_some_and(|p| self.ends_in_return(p)) {
268 UnreachableCause::AfterReturn
269 } else {
270 UnreachableCause::Other
271 };
272 self.unreachable_anchors.push((sid, cause));
273 return None;
274 };
275 cur = self.stmt(f, sid);
276 prev = Some(sid);
277 }
278 cur
279 }
280
281 fn ends_in_return(&self, sid: StmtId) -> bool {
288 match self.body.stmt(sid) {
289 Stmt::Return(_) => true,
290 Stmt::If {
291 then_branch,
292 elifs,
293 else_branch,
294 ..
295 } => {
296 let block_returns = |b: &[StmtId]| b.iter().any(|&s| self.ends_in_return(s));
297 else_branch.as_deref().is_some_and(block_returns)
298 && block_returns(then_branch)
299 && elifs.iter().all(|(_, b)| block_returns(b))
300 }
301 _ => false,
302 }
303 }
304
305 fn stmt(&mut self, facts: FlowFacts, sid: StmtId) -> Option<FlowFacts> {
308 self.entry_facts.insert(sid, facts.clone());
309 match self.body.stmt(sid) {
310 Stmt::Return(_) | Stmt::Break | Stmt::Continue => None,
311 Stmt::Pass | Stmt::Assert(_) => Some(facts),
312 Stmt::Expr(e) => Some(self.after_expr_stmt(facts, *e)),
313 Stmt::Var(v) => {
314 let mut f = facts;
315 f.invalidate_assigned(&Place::Local(v.name.clone()));
317 Some(f)
318 }
319 Stmt::If {
320 cond,
321 then_branch,
322 elifs,
323 else_branch,
324 } => self.flow_if(&facts, *cond, then_branch, elifs, else_branch.as_deref()),
325 Stmt::While { body, .. } => Some(self.flow_loop(facts, body, None)),
326 Stmt::For(f) => Some(self.flow_loop(facts, &f.body, Some(&f.var))),
327 Stmt::Match { arms, .. } => {
328 let mut after = facts.clone();
332 let mut saw_catch_all = false;
334 for arm in arms {
335 if saw_catch_all {
336 self.unreachable_pattern_anchors.push(arm.range);
337 }
338 let _ = self.block(facts.clone(), &arm.body);
339 self.scan_invalidations(&mut after, &arm.body);
340 saw_catch_all |= arm.is_catch_all;
341 }
342 Some(after)
343 }
344 }
345 }
346
347 fn after_expr_stmt(&self, mut facts: FlowFacts, e: ExprId) -> FlowFacts {
350 if let Expr::Bin {
351 op: BinOp::Assign,
352 lhs,
353 ..
354 } = self.body.expr(e)
355 && let Some(p) = Place::of(self.body, *lhs)
356 {
357 facts.invalidate_assigned(&p);
358 }
359 if self.expr_contains_call(e) {
360 facts.invalidate_self_rooted();
361 }
362 facts
363 }
364
365 fn flow_if(
369 &mut self,
370 facts: &FlowFacts,
371 cond: ExprId,
372 then_branch: &[StmtId],
373 elifs: &[(ExprId, crate::body::Block)],
374 else_branch: Option<&[StmtId]>,
375 ) -> Option<FlowFacts> {
376 let mut exits: Vec<Option<FlowFacts>> = Vec::new();
377 let then_in = self.apply(facts, cond, true);
378 exits.push(self.block(then_in, then_branch));
379
380 let mut chain = self.apply(facts, cond, false);
382 for (econd, eblock) in elifs {
383 let etrue = self.apply(&chain, *econd, true);
384 exits.push(self.block(etrue, eblock));
385 chain = self.apply(&chain, *econd, false);
386 }
387 exits.push(match else_branch {
389 Some(eb) => self.block(chain, eb),
390 None => Some(chain),
391 });
392
393 join_exits(exits)
394 }
395
396 fn flow_loop(
400 &mut self,
401 facts: FlowFacts,
402 body: &[StmtId],
403 loop_var: Option<&SmolStr>,
404 ) -> FlowFacts {
405 let mut widened = facts;
406 if let Some(v) = loop_var {
407 widened.invalidate_assigned(&Place::Local(v.clone()));
408 }
409 self.scan_invalidations(&mut widened, body);
410 let _ = self.block(widened.clone(), body);
413 widened
414 }
415
416 fn apply(&self, facts: &FlowFacts, cond: ExprId, truthy: bool) -> FlowFacts {
418 let mut out = facts.clone();
419 for (p, t) in self.derive_facts(cond, truthy) {
420 out.insert(p, t);
421 }
422 if self.expr_contains_call(cond) {
427 out.invalidate_self_rooted();
428 }
429 out
430 }
431
432 fn derive_facts(&self, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
434 match self.body.expr(cond) {
435 Expr::Paren(inner) => self.derive_facts(*inner, truthy),
436 Expr::Unary {
437 op: UnOp::Not,
438 operand,
439 } => self.derive_facts(*operand, !truthy),
440 Expr::Is {
441 operand,
442 ty: Some(ptr),
443 negated,
444 } => {
445 let positive = truthy != *negated;
446 Place::of(self.body, *operand)
447 .map(|p| {
448 let t = if positive {
449 NarrowedTy::Is(*ptr)
450 } else {
451 NarrowedTy::Not(*ptr)
452 };
453 vec![(p, t)]
454 })
455 .unwrap_or_default()
456 }
457 Expr::Bin {
458 op: BinOp::Eq,
459 lhs,
460 rhs,
461 } => self.null_cmp_facts(*lhs, *rhs, true, truthy),
462 Expr::Bin {
463 op: BinOp::Ne,
464 lhs,
465 rhs,
466 } => self.null_cmp_facts(*lhs, *rhs, false, truthy),
467 Expr::Bin {
470 op: BinOp::And,
471 lhs,
472 rhs,
473 } if truthy => {
474 let mut v = self.derive_facts(*lhs, true);
475 v.extend(self.derive_facts(*rhs, true));
476 v
477 }
478 Expr::Bin {
479 op: BinOp::Or,
480 lhs,
481 rhs,
482 } if !truthy => {
483 let mut v = self.derive_facts(*lhs, false);
484 v.extend(self.derive_facts(*rhs, false));
485 v
486 }
487 _ if truthy => Place::of(self.body, cond)
489 .map(|p| vec![(p, NarrowedTy::NotNull)])
490 .unwrap_or_default(),
491 _ => Vec::new(),
492 }
493 }
494
495 fn null_cmp_facts(
498 &self,
499 lhs: ExprId,
500 rhs: ExprId,
501 is_eq: bool,
502 truthy: bool,
503 ) -> Vec<(Place, NarrowedTy)> {
504 let other = if self.is_null(lhs) {
505 rhs
506 } else if self.is_null(rhs) {
507 lhs
508 } else {
509 return Vec::new();
510 };
511 let proves_not_null = if is_eq { !truthy } else { truthy };
512 if proves_not_null {
513 Place::of(self.body, other)
514 .map(|p| vec![(p, NarrowedTy::NotNull)])
515 .unwrap_or_default()
516 } else {
517 Vec::new()
518 }
519 }
520
521 fn is_null(&self, id: ExprId) -> bool {
522 matches!(self.body.expr(id), Expr::Literal(Literal::Null))
523 }
524
525 fn scan_invalidations(&self, facts: &mut FlowFacts, block: &[StmtId]) {
528 for &sid in block {
529 match self.body.stmt(sid) {
530 Stmt::Expr(e) => {
531 if let Expr::Bin {
532 op: BinOp::Assign,
533 lhs,
534 ..
535 } = self.body.expr(*e)
536 && let Some(p) = Place::of(self.body, *lhs)
537 {
538 facts.invalidate_assigned(&p);
539 }
540 if self.expr_contains_call(*e) {
541 facts.invalidate_self_rooted();
542 }
543 }
544 Stmt::Var(v) => facts.invalidate_assigned(&Place::Local(v.name.clone())),
545 Stmt::If {
546 cond,
547 then_branch,
548 elifs,
549 else_branch,
550 } => {
551 if self.expr_contains_call(*cond) {
554 facts.invalidate_self_rooted();
555 }
556 self.scan_invalidations(facts, then_branch);
557 for (econd, b) in elifs {
558 if self.expr_contains_call(*econd) {
559 facts.invalidate_self_rooted();
560 }
561 self.scan_invalidations(facts, b);
562 }
563 if let Some(eb) = else_branch {
564 self.scan_invalidations(facts, eb);
565 }
566 }
567 Stmt::While { cond, body } => {
568 if self.expr_contains_call(*cond) {
569 facts.invalidate_self_rooted();
570 }
571 self.scan_invalidations(facts, body);
572 }
573 Stmt::For(f) => {
574 facts.invalidate_assigned(&Place::Local(f.var.clone()));
575 if self.expr_contains_call(f.iter) {
576 facts.invalidate_self_rooted();
577 }
578 self.scan_invalidations(facts, &f.body);
579 }
580 Stmt::Match { scrutinee, arms } => {
581 if self.expr_contains_call(*scrutinee) {
582 facts.invalidate_self_rooted();
583 }
584 for arm in arms {
585 self.scan_invalidations(facts, &arm.body);
586 }
587 }
588 Stmt::Assert(Some(c)) => {
589 if self.expr_contains_call(*c) {
590 facts.invalidate_self_rooted();
591 }
592 }
593 Stmt::Return(_)
594 | Stmt::Break
595 | Stmt::Continue
596 | Stmt::Pass
597 | Stmt::Assert(None) => {}
598 }
599 }
600 }
601
602 fn expr_contains_call(&self, id: ExprId) -> bool {
604 match self.body.expr(id) {
605 Expr::Call { .. } => true,
606 Expr::Bin { lhs, rhs, .. } | Expr::In { lhs, rhs, .. } => {
607 self.expr_contains_call(*lhs) || self.expr_contains_call(*rhs)
608 }
609 Expr::Unary { operand, .. }
610 | Expr::Await(operand)
611 | Expr::Paren(operand)
612 | Expr::Cast { operand, .. }
613 | Expr::Is { operand, .. } => self.expr_contains_call(*operand),
614 Expr::Ternary {
615 cond,
616 then_branch,
617 else_branch,
618 } => {
619 self.expr_contains_call(*cond)
620 || self.expr_contains_call(*then_branch)
621 || self.expr_contains_call(*else_branch)
622 }
623 Expr::Field { receiver, .. } => self.expr_contains_call(*receiver),
624 Expr::Index { base, index } => {
625 self.expr_contains_call(*base) || self.expr_contains_call(*index)
626 }
627 Expr::Array(items) => items.iter().any(|&e| self.expr_contains_call(e)),
628 Expr::Dict(entries) => entries.iter().any(|(k, v)| {
629 self.expr_contains_call(*k) || v.is_some_and(|e| self.expr_contains_call(e))
630 }),
631 _ => false,
632 }
633 }
634}
635
636#[must_use]
640pub fn condition_facts(body: &Body, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
641 Analyzer {
642 body,
643 entry_facts: FxHashMap::default(),
644 unreachable_anchors: Vec::new(),
645 unreachable_pattern_anchors: Vec::new(),
646 }
647 .derive_facts(cond, truthy)
648}
649
650fn join_exits(exits: Vec<Option<FlowFacts>>) -> Option<FlowFacts> {
653 let mut iter = exits.into_iter().flatten();
654 let first = iter.next()?;
655 Some(iter.fold(first, |acc, f| acc.join(&f)))
656}
657
658#[derive(Debug, Clone, Default)]
666pub struct AssignedAnalysis {
667 entry: FxHashMap<StmtId, FxHashSet<SmolStr>>,
668}
669
670impl AssignedAnalysis {
671 #[must_use]
674 pub fn assigned_before(&self, stmt: StmtId) -> Option<&FxHashSet<SmolStr>> {
675 self.entry.get(&stmt)
676 }
677}
678
679#[must_use]
683pub fn analyze_assigned(body: &Body, params: &[SmolStr]) -> AssignedAnalysis {
684 let mut a = AssignAnalyzer {
685 body,
686 entry: FxHashMap::default(),
687 };
688 let seed: FxHashSet<SmolStr> = params.iter().cloned().collect();
689 a.block(seed, &body.block);
690 AssignedAnalysis { entry: a.entry }
691}
692
693struct AssignAnalyzer<'a> {
694 body: &'a Body,
695 entry: FxHashMap<StmtId, FxHashSet<SmolStr>>,
696}
697
698impl AssignAnalyzer<'_> {
699 fn block(
701 &mut self,
702 assigned: FxHashSet<SmolStr>,
703 block: &[StmtId],
704 ) -> Option<FxHashSet<SmolStr>> {
705 let mut cur = Some(assigned);
706 for &sid in block {
707 let a = cur?;
708 cur = self.stmt(a, sid);
709 }
710 cur
711 }
712
713 fn stmt(&mut self, assigned: FxHashSet<SmolStr>, sid: StmtId) -> Option<FxHashSet<SmolStr>> {
714 self.entry.insert(sid, assigned.clone());
715 match self.body.stmt(sid) {
716 Stmt::Return(_) | Stmt::Break | Stmt::Continue => None,
717 Stmt::Pass | Stmt::Assert(_) => Some(assigned),
718 Stmt::Expr(e) => {
719 let mut a = assigned;
720 self.record_assign(&mut a, *e);
721 Some(a)
722 }
723 Stmt::Var(v) => {
726 let mut a = assigned;
727 if v.init.is_some() {
728 a.insert(v.name.clone());
729 } else {
730 a.remove(&v.name);
731 }
732 Some(a)
733 }
734 Stmt::If {
735 then_branch,
736 elifs,
737 else_branch,
738 ..
739 } => {
740 let mut exits = vec![self.block(assigned.clone(), then_branch)];
741 for (_, eblock) in elifs {
742 exits.push(self.block(assigned.clone(), eblock));
743 }
744 exits.push(match else_branch {
745 Some(eb) => self.block(assigned.clone(), eb),
746 None => Some(assigned.clone()),
747 });
748 intersect_exits(exits)
749 }
750 Stmt::While { body, .. } => {
752 let _ = self.block(assigned.clone(), body);
753 Some(assigned)
754 }
755 Stmt::For(f) => {
756 let mut body_in = assigned.clone();
758 body_in.insert(f.var.clone());
759 let _ = self.block(body_in, &f.body);
760 Some(assigned)
761 }
762 Stmt::Match { arms, .. } => {
765 for arm in arms {
766 let mut arm_in = assigned.clone();
767 for b in &arm.binds {
768 arm_in.insert(b.name.clone());
769 }
770 let _ = self.block(arm_in, &arm.body);
771 }
772 Some(assigned)
773 }
774 }
775 }
776
777 fn record_assign(&self, assigned: &mut FxHashSet<SmolStr>, e: ExprId) {
779 if let Expr::Bin {
780 op: BinOp::Assign,
781 lhs,
782 ..
783 } = self.body.expr(e)
784 && let Expr::Name(n) = self.body.expr(*lhs)
785 {
786 assigned.insert(n.clone());
787 }
788 }
789}
790
791fn intersect_exits(exits: Vec<Option<FxHashSet<SmolStr>>>) -> Option<FxHashSet<SmolStr>> {
794 let mut iter = exits.into_iter().flatten();
795 let first = iter.next()?;
796 Some(iter.fold(first, |acc, s| acc.intersection(&s).cloned().collect()))
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::body::{self, Body};
803 use gdscript_syntax::{SyntaxKind, ast, parse};
804
805 fn func_body(src: &str) -> Body {
806 let root = parse(src).syntax_node();
807 let func = ast::descendants(&root)
808 .into_iter()
809 .find(|n| n.kind() == SyntaxKind::FuncDecl)
810 .expect("a FuncDecl");
811 body::body_of_func(&func)
812 }
813
814 fn fact_at(body: &Body, a: &FlowAnalysis, i: usize) -> Option<(Place, NarrowedTy)> {
816 let sid = body.block[i];
817 let facts = a.facts_before(sid)?;
818 facts.0.iter().next().map(|(p, t)| (p.clone(), t.clone()))
819 }
820
821 #[test]
822 fn is_guard_narrows_then_branch() {
823 let body = func_body("func f(x):\n\tif x is Node:\n\t\tx.free()\n");
824 let a = analyze(&body);
825 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
827 panic!("if")
828 };
829 let inner = a.facts_before(then_branch[0]).expect("then facts");
830 assert_eq!(
831 inner.get(&Place::Local("x".into())),
832 Some(&NarrowedTy::Is(match body.stmt(body.block[0]) {
833 Stmt::If { cond, .. } => match body.expr(*cond) {
834 Expr::Is { ty: Some(p), .. } => *p,
835 _ => panic!("is"),
836 },
837 _ => unreachable!(),
838 })),
839 );
840 }
841
842 #[test]
843 fn early_return_narrows_after_the_guard() {
844 let body = func_body("func f(x):\n\tif x == null:\n\t\treturn\n\tx.free()\n");
846 let a = analyze(&body);
847 let after = a.facts_before(body.block[1]).expect("after-if facts");
849 assert_eq!(
850 after.get(&Place::Local("x".into())),
851 Some(&NarrowedTy::NotNull)
852 );
853 }
854
855 #[test]
856 fn code_after_return_is_unreachable() {
857 let body = func_body("func f():\n\treturn\n\tvar dead := 1\n");
858 let a = analyze(&body);
859 assert_eq!(a.unreachable_ranges(&body).len(), 1);
860 assert_eq!(
862 a.unreachable_anchors,
863 vec![(body.block[1], UnreachableCause::AfterReturn)]
864 );
865 }
866
867 #[test]
868 fn code_after_break_is_unreachable_with_other_cause() {
869 let body = func_body("func f():\n\tfor i in 3:\n\t\tbreak\n\t\tprint(i)\n");
872 let a = analyze(&body);
873 let anchors = &a.unreachable_anchors;
874 assert_eq!(anchors.len(), 1);
875 assert_eq!(anchors[0].1, UnreachableCause::Other);
876 }
877
878 #[test]
879 fn code_after_if_where_every_branch_returns_is_return_caused() {
880 let body = func_body(
882 "func f(b):\n\tif b:\n\t\treturn 1\n\telse:\n\t\treturn 2\n\tprint(\"after\")\n",
883 );
884 let a = analyze(&body);
885 let anchors = &a.unreachable_anchors;
886 assert_eq!(anchors.len(), 1);
887 assert_eq!(anchors[0].1, UnreachableCause::AfterReturn);
888 }
889
890 #[test]
891 fn code_after_if_with_a_break_exit_is_other_caused() {
892 let body = func_body(
894 "func f(b):\n\tfor i in 3:\n\t\tif b:\n\t\t\treturn 1\n\t\telse:\n\t\t\tbreak\n\t\tprint(i)\n",
895 );
896 let a = analyze(&body);
897 let anchors = &a.unreachable_anchors;
898 assert_eq!(anchors.len(), 1);
899 assert_eq!(anchors[0].1, UnreachableCause::Other);
900 }
901
902 #[test]
903 fn reassignment_invalidates_narrowing() {
904 let body = func_body("func f(x, other):\n\tif x is Node:\n\t\tx = other\n\t\tx.free()\n");
906 let a = analyze(&body);
907 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
908 panic!("if")
909 };
910 let at_free = a.facts_before(then_branch[1]).expect("facts");
912 assert_eq!(at_free.get(&Place::Local("x".into())), None);
913 }
914
915 #[test]
916 fn opaque_call_invalidates_self_members() {
917 let body =
919 func_body("func f():\n\tif self.node is Node2D:\n\t\tmutate()\n\t\tself.node.foo()\n");
920 let a = analyze(&body);
921 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
922 panic!("if")
923 };
924 let at_use = a.facts_before(then_branch[1]).expect("facts");
925 assert_eq!(at_use.get(&Place::SelfMember("node".into())), None);
926 }
927
928 #[test]
929 fn opaque_call_in_guard_invalidates_self_member_narrowing() {
930 let body =
933 func_body("func f():\n\tif self.node is Node2D and mutate():\n\t\tself.node.foo()\n");
934 let a = analyze(&body);
935 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
936 panic!("if")
937 };
938 let inner = a.facts_before(then_branch[0]).expect("then facts");
939 assert_eq!(inner.get(&Place::SelfMember("node".into())), None);
940 }
941
942 #[test]
943 fn merge_drops_disagreeing_facts() {
944 let body =
946 func_body("func f(x):\n\tif x is Node:\n\t\tpass\n\telse:\n\t\tpass\n\tx.free()\n");
947 let a = analyze(&body);
948 let after = fact_at(&body, &a, 1);
949 assert!(
950 after.is_none(),
951 "narrowing must not survive a non-exhaustive merge"
952 );
953 }
954
955 #[test]
956 fn and_short_circuit_narrows_rhs_and_after() {
957 let body = func_body("func f(x):\n\tif x is Node and true:\n\t\tx.free()\n");
959 let a = analyze(&body);
960 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
961 panic!("if")
962 };
963 let inner = a.facts_before(then_branch[0]).expect("then facts");
964 assert!(matches!(
965 inner.get(&Place::Local("x".into())),
966 Some(NarrowedTy::Is(_))
967 ));
968 }
969
970 #[test]
971 fn loop_body_is_entered_widened() {
972 let body = func_body(
974 "func f(x, other):\n\tif x is Node:\n\t\twhile true:\n\t\t\tx = other\n\t\t\tx.free()\n",
975 );
976 let a = analyze(&body);
977 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
979 panic!("if")
980 };
981 assert!(a.facts_before(then_branch[0]).is_some());
982 }
983}