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, Default)]
177pub struct FlowAnalysis {
178 entry_facts: FxHashMap<StmtId, FlowFacts>,
181 unreachable_anchors: Vec<StmtId>,
183 unreachable_pattern_anchors: Vec<TextRange>,
186}
187
188impl FlowAnalysis {
189 #[must_use]
191 pub fn facts_before(&self, stmt: StmtId) -> Option<&FlowFacts> {
192 self.entry_facts.get(&stmt)
193 }
194
195 #[must_use]
197 pub fn unreachable_ranges(&self, body: &Body) -> Vec<TextRange> {
198 self.unreachable_anchors
199 .iter()
200 .map(|&sid| body.source_map.stmt_range(sid))
201 .collect()
202 }
203
204 #[must_use]
206 pub fn unreachable_pattern_ranges(&self) -> &[TextRange] {
207 &self.unreachable_pattern_anchors
208 }
209}
210
211#[must_use]
213pub fn analyze(body: &Body) -> FlowAnalysis {
214 let mut a = Analyzer {
215 body,
216 entry_facts: FxHashMap::default(),
217 unreachable_anchors: Vec::new(),
218 unreachable_pattern_anchors: Vec::new(),
219 };
220 a.block(FlowFacts::default(), &body.block);
221 for expr in &body.exprs {
225 if let Expr::Lambda { body: lbody, .. } = expr {
226 a.block(FlowFacts::default(), lbody);
227 }
228 }
229 FlowAnalysis {
230 entry_facts: a.entry_facts,
231 unreachable_anchors: a.unreachable_anchors,
232 unreachable_pattern_anchors: a.unreachable_pattern_anchors,
233 }
234}
235
236struct Analyzer<'a> {
237 body: &'a Body,
238 entry_facts: FxHashMap<StmtId, FlowFacts>,
239 unreachable_anchors: Vec<StmtId>,
240 unreachable_pattern_anchors: Vec<TextRange>,
241}
242
243impl Analyzer<'_> {
244 fn block(&mut self, facts: FlowFacts, block: &[StmtId]) -> Option<FlowFacts> {
248 let mut cur = Some(facts);
249 for &sid in block {
250 let Some(f) = cur else {
251 self.unreachable_anchors.push(sid);
253 return None;
254 };
255 cur = self.stmt(f, sid);
256 }
257 cur
258 }
259
260 fn stmt(&mut self, facts: FlowFacts, sid: StmtId) -> Option<FlowFacts> {
263 self.entry_facts.insert(sid, facts.clone());
264 match self.body.stmt(sid) {
265 Stmt::Return(_) | Stmt::Break | Stmt::Continue => None,
266 Stmt::Pass | Stmt::Assert(_) => Some(facts),
267 Stmt::Expr(e) => Some(self.after_expr_stmt(facts, *e)),
268 Stmt::Var(v) => {
269 let mut f = facts;
270 f.invalidate_assigned(&Place::Local(v.name.clone()));
272 Some(f)
273 }
274 Stmt::If {
275 cond,
276 then_branch,
277 elifs,
278 else_branch,
279 } => self.flow_if(&facts, *cond, then_branch, elifs, else_branch.as_deref()),
280 Stmt::While { body, .. } => Some(self.flow_loop(facts, body, None)),
281 Stmt::For(f) => Some(self.flow_loop(facts, &f.body, Some(&f.var))),
282 Stmt::Match { arms, .. } => {
283 let mut after = facts.clone();
287 let mut saw_catch_all = false;
289 for arm in arms {
290 if saw_catch_all {
291 self.unreachable_pattern_anchors.push(arm.range);
292 }
293 let _ = self.block(facts.clone(), &arm.body);
294 self.scan_invalidations(&mut after, &arm.body);
295 saw_catch_all |= arm.is_catch_all;
296 }
297 Some(after)
298 }
299 }
300 }
301
302 fn after_expr_stmt(&self, mut facts: FlowFacts, e: ExprId) -> FlowFacts {
305 if let Expr::Bin {
306 op: BinOp::Assign,
307 lhs,
308 ..
309 } = self.body.expr(e)
310 && let Some(p) = Place::of(self.body, *lhs)
311 {
312 facts.invalidate_assigned(&p);
313 }
314 if self.expr_contains_call(e) {
315 facts.invalidate_self_rooted();
316 }
317 facts
318 }
319
320 fn flow_if(
324 &mut self,
325 facts: &FlowFacts,
326 cond: ExprId,
327 then_branch: &[StmtId],
328 elifs: &[(ExprId, crate::body::Block)],
329 else_branch: Option<&[StmtId]>,
330 ) -> Option<FlowFacts> {
331 let mut exits: Vec<Option<FlowFacts>> = Vec::new();
332 let then_in = self.apply(facts, cond, true);
333 exits.push(self.block(then_in, then_branch));
334
335 let mut chain = self.apply(facts, cond, false);
337 for (econd, eblock) in elifs {
338 let etrue = self.apply(&chain, *econd, true);
339 exits.push(self.block(etrue, eblock));
340 chain = self.apply(&chain, *econd, false);
341 }
342 exits.push(match else_branch {
344 Some(eb) => self.block(chain, eb),
345 None => Some(chain),
346 });
347
348 join_exits(exits)
349 }
350
351 fn flow_loop(
355 &mut self,
356 facts: FlowFacts,
357 body: &[StmtId],
358 loop_var: Option<&SmolStr>,
359 ) -> FlowFacts {
360 let mut widened = facts;
361 if let Some(v) = loop_var {
362 widened.invalidate_assigned(&Place::Local(v.clone()));
363 }
364 self.scan_invalidations(&mut widened, body);
365 let _ = self.block(widened.clone(), body);
368 widened
369 }
370
371 fn apply(&self, facts: &FlowFacts, cond: ExprId, truthy: bool) -> FlowFacts {
373 let mut out = facts.clone();
374 for (p, t) in self.derive_facts(cond, truthy) {
375 out.insert(p, t);
376 }
377 if self.expr_contains_call(cond) {
382 out.invalidate_self_rooted();
383 }
384 out
385 }
386
387 fn derive_facts(&self, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
389 match self.body.expr(cond) {
390 Expr::Paren(inner) => self.derive_facts(*inner, truthy),
391 Expr::Unary {
392 op: UnOp::Not,
393 operand,
394 } => self.derive_facts(*operand, !truthy),
395 Expr::Is {
396 operand,
397 ty: Some(ptr),
398 negated,
399 } => {
400 let positive = truthy != *negated;
401 Place::of(self.body, *operand)
402 .map(|p| {
403 let t = if positive {
404 NarrowedTy::Is(*ptr)
405 } else {
406 NarrowedTy::Not(*ptr)
407 };
408 vec![(p, t)]
409 })
410 .unwrap_or_default()
411 }
412 Expr::Bin {
413 op: BinOp::Eq,
414 lhs,
415 rhs,
416 } => self.null_cmp_facts(*lhs, *rhs, true, truthy),
417 Expr::Bin {
418 op: BinOp::Ne,
419 lhs,
420 rhs,
421 } => self.null_cmp_facts(*lhs, *rhs, false, truthy),
422 Expr::Bin {
425 op: BinOp::And,
426 lhs,
427 rhs,
428 } if truthy => {
429 let mut v = self.derive_facts(*lhs, true);
430 v.extend(self.derive_facts(*rhs, true));
431 v
432 }
433 Expr::Bin {
434 op: BinOp::Or,
435 lhs,
436 rhs,
437 } if !truthy => {
438 let mut v = self.derive_facts(*lhs, false);
439 v.extend(self.derive_facts(*rhs, false));
440 v
441 }
442 _ if truthy => Place::of(self.body, cond)
444 .map(|p| vec![(p, NarrowedTy::NotNull)])
445 .unwrap_or_default(),
446 _ => Vec::new(),
447 }
448 }
449
450 fn null_cmp_facts(
453 &self,
454 lhs: ExprId,
455 rhs: ExprId,
456 is_eq: bool,
457 truthy: bool,
458 ) -> Vec<(Place, NarrowedTy)> {
459 let other = if self.is_null(lhs) {
460 rhs
461 } else if self.is_null(rhs) {
462 lhs
463 } else {
464 return Vec::new();
465 };
466 let proves_not_null = if is_eq { !truthy } else { truthy };
467 if proves_not_null {
468 Place::of(self.body, other)
469 .map(|p| vec![(p, NarrowedTy::NotNull)])
470 .unwrap_or_default()
471 } else {
472 Vec::new()
473 }
474 }
475
476 fn is_null(&self, id: ExprId) -> bool {
477 matches!(self.body.expr(id), Expr::Literal(Literal::Null))
478 }
479
480 fn scan_invalidations(&self, facts: &mut FlowFacts, block: &[StmtId]) {
483 for &sid in block {
484 match self.body.stmt(sid) {
485 Stmt::Expr(e) => {
486 if let Expr::Bin {
487 op: BinOp::Assign,
488 lhs,
489 ..
490 } = self.body.expr(*e)
491 && let Some(p) = Place::of(self.body, *lhs)
492 {
493 facts.invalidate_assigned(&p);
494 }
495 if self.expr_contains_call(*e) {
496 facts.invalidate_self_rooted();
497 }
498 }
499 Stmt::Var(v) => facts.invalidate_assigned(&Place::Local(v.name.clone())),
500 Stmt::If {
501 cond,
502 then_branch,
503 elifs,
504 else_branch,
505 } => {
506 if self.expr_contains_call(*cond) {
509 facts.invalidate_self_rooted();
510 }
511 self.scan_invalidations(facts, then_branch);
512 for (econd, b) in elifs {
513 if self.expr_contains_call(*econd) {
514 facts.invalidate_self_rooted();
515 }
516 self.scan_invalidations(facts, b);
517 }
518 if let Some(eb) = else_branch {
519 self.scan_invalidations(facts, eb);
520 }
521 }
522 Stmt::While { cond, body } => {
523 if self.expr_contains_call(*cond) {
524 facts.invalidate_self_rooted();
525 }
526 self.scan_invalidations(facts, body);
527 }
528 Stmt::For(f) => {
529 facts.invalidate_assigned(&Place::Local(f.var.clone()));
530 if self.expr_contains_call(f.iter) {
531 facts.invalidate_self_rooted();
532 }
533 self.scan_invalidations(facts, &f.body);
534 }
535 Stmt::Match { scrutinee, arms } => {
536 if self.expr_contains_call(*scrutinee) {
537 facts.invalidate_self_rooted();
538 }
539 for arm in arms {
540 self.scan_invalidations(facts, &arm.body);
541 }
542 }
543 Stmt::Assert(Some(c)) => {
544 if self.expr_contains_call(*c) {
545 facts.invalidate_self_rooted();
546 }
547 }
548 Stmt::Return(_)
549 | Stmt::Break
550 | Stmt::Continue
551 | Stmt::Pass
552 | Stmt::Assert(None) => {}
553 }
554 }
555 }
556
557 fn expr_contains_call(&self, id: ExprId) -> bool {
559 match self.body.expr(id) {
560 Expr::Call { .. } => true,
561 Expr::Bin { lhs, rhs, .. } | Expr::In { lhs, rhs, .. } => {
562 self.expr_contains_call(*lhs) || self.expr_contains_call(*rhs)
563 }
564 Expr::Unary { operand, .. }
565 | Expr::Await(operand)
566 | Expr::Paren(operand)
567 | Expr::Cast { operand, .. }
568 | Expr::Is { operand, .. } => self.expr_contains_call(*operand),
569 Expr::Ternary {
570 cond,
571 then_branch,
572 else_branch,
573 } => {
574 self.expr_contains_call(*cond)
575 || self.expr_contains_call(*then_branch)
576 || self.expr_contains_call(*else_branch)
577 }
578 Expr::Field { receiver, .. } => self.expr_contains_call(*receiver),
579 Expr::Index { base, index } => {
580 self.expr_contains_call(*base) || self.expr_contains_call(*index)
581 }
582 Expr::Array(items) => items.iter().any(|&e| self.expr_contains_call(e)),
583 Expr::Dict(entries) => entries.iter().any(|(k, v)| {
584 self.expr_contains_call(*k) || v.is_some_and(|e| self.expr_contains_call(e))
585 }),
586 _ => false,
587 }
588 }
589}
590
591#[must_use]
595pub fn condition_facts(body: &Body, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
596 Analyzer {
597 body,
598 entry_facts: FxHashMap::default(),
599 unreachable_anchors: Vec::new(),
600 unreachable_pattern_anchors: Vec::new(),
601 }
602 .derive_facts(cond, truthy)
603}
604
605fn join_exits(exits: Vec<Option<FlowFacts>>) -> Option<FlowFacts> {
608 let mut iter = exits.into_iter().flatten();
609 let first = iter.next()?;
610 Some(iter.fold(first, |acc, f| acc.join(&f)))
611}
612
613#[derive(Debug, Clone, Default)]
621pub struct AssignedAnalysis {
622 entry: FxHashMap<StmtId, FxHashSet<SmolStr>>,
623}
624
625impl AssignedAnalysis {
626 #[must_use]
629 pub fn assigned_before(&self, stmt: StmtId) -> Option<&FxHashSet<SmolStr>> {
630 self.entry.get(&stmt)
631 }
632}
633
634#[must_use]
638pub fn analyze_assigned(body: &Body, params: &[SmolStr]) -> AssignedAnalysis {
639 let mut a = AssignAnalyzer {
640 body,
641 entry: FxHashMap::default(),
642 };
643 let seed: FxHashSet<SmolStr> = params.iter().cloned().collect();
644 a.block(seed, &body.block);
645 AssignedAnalysis { entry: a.entry }
646}
647
648struct AssignAnalyzer<'a> {
649 body: &'a Body,
650 entry: FxHashMap<StmtId, FxHashSet<SmolStr>>,
651}
652
653impl AssignAnalyzer<'_> {
654 fn block(
656 &mut self,
657 assigned: FxHashSet<SmolStr>,
658 block: &[StmtId],
659 ) -> Option<FxHashSet<SmolStr>> {
660 let mut cur = Some(assigned);
661 for &sid in block {
662 let a = cur?;
663 cur = self.stmt(a, sid);
664 }
665 cur
666 }
667
668 fn stmt(&mut self, assigned: FxHashSet<SmolStr>, sid: StmtId) -> Option<FxHashSet<SmolStr>> {
669 self.entry.insert(sid, assigned.clone());
670 match self.body.stmt(sid) {
671 Stmt::Return(_) | Stmt::Break | Stmt::Continue => None,
672 Stmt::Pass | Stmt::Assert(_) => Some(assigned),
673 Stmt::Expr(e) => {
674 let mut a = assigned;
675 self.record_assign(&mut a, *e);
676 Some(a)
677 }
678 Stmt::Var(v) => {
681 let mut a = assigned;
682 if v.init.is_some() {
683 a.insert(v.name.clone());
684 } else {
685 a.remove(&v.name);
686 }
687 Some(a)
688 }
689 Stmt::If {
690 then_branch,
691 elifs,
692 else_branch,
693 ..
694 } => {
695 let mut exits = vec![self.block(assigned.clone(), then_branch)];
696 for (_, eblock) in elifs {
697 exits.push(self.block(assigned.clone(), eblock));
698 }
699 exits.push(match else_branch {
700 Some(eb) => self.block(assigned.clone(), eb),
701 None => Some(assigned.clone()),
702 });
703 intersect_exits(exits)
704 }
705 Stmt::While { body, .. } => {
707 let _ = self.block(assigned.clone(), body);
708 Some(assigned)
709 }
710 Stmt::For(f) => {
711 let mut body_in = assigned.clone();
713 body_in.insert(f.var.clone());
714 let _ = self.block(body_in, &f.body);
715 Some(assigned)
716 }
717 Stmt::Match { arms, .. } => {
720 for arm in arms {
721 let mut arm_in = assigned.clone();
722 for b in &arm.binds {
723 arm_in.insert(b.name.clone());
724 }
725 let _ = self.block(arm_in, &arm.body);
726 }
727 Some(assigned)
728 }
729 }
730 }
731
732 fn record_assign(&self, assigned: &mut FxHashSet<SmolStr>, e: ExprId) {
734 if let Expr::Bin {
735 op: BinOp::Assign,
736 lhs,
737 ..
738 } = self.body.expr(e)
739 && let Expr::Name(n) = self.body.expr(*lhs)
740 {
741 assigned.insert(n.clone());
742 }
743 }
744}
745
746fn intersect_exits(exits: Vec<Option<FxHashSet<SmolStr>>>) -> Option<FxHashSet<SmolStr>> {
749 let mut iter = exits.into_iter().flatten();
750 let first = iter.next()?;
751 Some(iter.fold(first, |acc, s| acc.intersection(&s).cloned().collect()))
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757 use crate::body::{self, Body};
758 use gdscript_syntax::{SyntaxKind, ast, parse};
759
760 fn func_body(src: &str) -> Body {
761 let root = parse(src).syntax_node();
762 let func = ast::descendants(&root)
763 .into_iter()
764 .find(|n| n.kind() == SyntaxKind::FuncDecl)
765 .expect("a FuncDecl");
766 body::body_of_func(&func)
767 }
768
769 fn fact_at(body: &Body, a: &FlowAnalysis, i: usize) -> Option<(Place, NarrowedTy)> {
771 let sid = body.block[i];
772 let facts = a.facts_before(sid)?;
773 facts.0.iter().next().map(|(p, t)| (p.clone(), t.clone()))
774 }
775
776 #[test]
777 fn is_guard_narrows_then_branch() {
778 let body = func_body("func f(x):\n\tif x is Node:\n\t\tx.free()\n");
779 let a = analyze(&body);
780 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
782 panic!("if")
783 };
784 let inner = a.facts_before(then_branch[0]).expect("then facts");
785 assert_eq!(
786 inner.get(&Place::Local("x".into())),
787 Some(&NarrowedTy::Is(match body.stmt(body.block[0]) {
788 Stmt::If { cond, .. } => match body.expr(*cond) {
789 Expr::Is { ty: Some(p), .. } => *p,
790 _ => panic!("is"),
791 },
792 _ => unreachable!(),
793 })),
794 );
795 }
796
797 #[test]
798 fn early_return_narrows_after_the_guard() {
799 let body = func_body("func f(x):\n\tif x == null:\n\t\treturn\n\tx.free()\n");
801 let a = analyze(&body);
802 let after = a.facts_before(body.block[1]).expect("after-if facts");
804 assert_eq!(
805 after.get(&Place::Local("x".into())),
806 Some(&NarrowedTy::NotNull)
807 );
808 }
809
810 #[test]
811 fn code_after_return_is_unreachable() {
812 let body = func_body("func f():\n\treturn\n\tvar dead := 1\n");
813 let a = analyze(&body);
814 assert_eq!(a.unreachable_ranges(&body).len(), 1);
815 assert_eq!(a.unreachable_anchors, vec![body.block[1]]);
817 }
818
819 #[test]
820 fn reassignment_invalidates_narrowing() {
821 let body = func_body("func f(x, other):\n\tif x is Node:\n\t\tx = other\n\t\tx.free()\n");
823 let a = analyze(&body);
824 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
825 panic!("if")
826 };
827 let at_free = a.facts_before(then_branch[1]).expect("facts");
829 assert_eq!(at_free.get(&Place::Local("x".into())), None);
830 }
831
832 #[test]
833 fn opaque_call_invalidates_self_members() {
834 let body =
836 func_body("func f():\n\tif self.node is Node2D:\n\t\tmutate()\n\t\tself.node.foo()\n");
837 let a = analyze(&body);
838 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
839 panic!("if")
840 };
841 let at_use = a.facts_before(then_branch[1]).expect("facts");
842 assert_eq!(at_use.get(&Place::SelfMember("node".into())), None);
843 }
844
845 #[test]
846 fn opaque_call_in_guard_invalidates_self_member_narrowing() {
847 let body =
850 func_body("func f():\n\tif self.node is Node2D and mutate():\n\t\tself.node.foo()\n");
851 let a = analyze(&body);
852 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
853 panic!("if")
854 };
855 let inner = a.facts_before(then_branch[0]).expect("then facts");
856 assert_eq!(inner.get(&Place::SelfMember("node".into())), None);
857 }
858
859 #[test]
860 fn merge_drops_disagreeing_facts() {
861 let body =
863 func_body("func f(x):\n\tif x is Node:\n\t\tpass\n\telse:\n\t\tpass\n\tx.free()\n");
864 let a = analyze(&body);
865 let after = fact_at(&body, &a, 1);
866 assert!(
867 after.is_none(),
868 "narrowing must not survive a non-exhaustive merge"
869 );
870 }
871
872 #[test]
873 fn and_short_circuit_narrows_rhs_and_after() {
874 let body = func_body("func f(x):\n\tif x is Node and true:\n\t\tx.free()\n");
876 let a = analyze(&body);
877 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
878 panic!("if")
879 };
880 let inner = a.facts_before(then_branch[0]).expect("then facts");
881 assert!(matches!(
882 inner.get(&Place::Local("x".into())),
883 Some(NarrowedTy::Is(_))
884 ));
885 }
886
887 #[test]
888 fn loop_body_is_entered_widened() {
889 let body = func_body(
891 "func f(x, other):\n\tif x is Node:\n\t\twhile true:\n\t\t\tx = other\n\t\t\tx.free()\n",
892 );
893 let a = analyze(&body);
894 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
896 panic!("if")
897 };
898 assert!(a.facts_before(then_branch[0]).is_some());
899 }
900}