1use rustc_hash::FxHashMap;
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}
184
185impl FlowAnalysis {
186 #[must_use]
188 pub fn facts_before(&self, stmt: StmtId) -> Option<&FlowFacts> {
189 self.entry_facts.get(&stmt)
190 }
191
192 #[must_use]
194 pub fn unreachable_ranges(&self, body: &Body) -> Vec<TextRange> {
195 self.unreachable_anchors
196 .iter()
197 .map(|&sid| body.source_map.stmt_range(sid))
198 .collect()
199 }
200}
201
202#[must_use]
204pub fn analyze(body: &Body) -> FlowAnalysis {
205 let mut a = Analyzer {
206 body,
207 entry_facts: FxHashMap::default(),
208 unreachable_anchors: Vec::new(),
209 };
210 a.block(FlowFacts::default(), &body.block);
211 for expr in &body.exprs {
215 if let Expr::Lambda { body: lbody, .. } = expr {
216 a.block(FlowFacts::default(), lbody);
217 }
218 }
219 FlowAnalysis {
220 entry_facts: a.entry_facts,
221 unreachable_anchors: a.unreachable_anchors,
222 }
223}
224
225struct Analyzer<'a> {
226 body: &'a Body,
227 entry_facts: FxHashMap<StmtId, FlowFacts>,
228 unreachable_anchors: Vec<StmtId>,
229}
230
231impl Analyzer<'_> {
232 fn block(&mut self, facts: FlowFacts, block: &[StmtId]) -> Option<FlowFacts> {
236 let mut cur = Some(facts);
237 for &sid in block {
238 let Some(f) = cur else {
239 self.unreachable_anchors.push(sid);
241 return None;
242 };
243 cur = self.stmt(f, sid);
244 }
245 cur
246 }
247
248 fn stmt(&mut self, facts: FlowFacts, sid: StmtId) -> Option<FlowFacts> {
251 self.entry_facts.insert(sid, facts.clone());
252 match self.body.stmt(sid) {
253 Stmt::Return(_) | Stmt::Break | Stmt::Continue => None,
254 Stmt::Pass | Stmt::Assert(_) => Some(facts),
255 Stmt::Expr(e) => Some(self.after_expr_stmt(facts, *e)),
256 Stmt::Var(v) => {
257 let mut f = facts;
258 f.invalidate_assigned(&Place::Local(v.name.clone()));
260 Some(f)
261 }
262 Stmt::If {
263 cond,
264 then_branch,
265 elifs,
266 else_branch,
267 } => self.flow_if(&facts, *cond, then_branch, elifs, else_branch.as_deref()),
268 Stmt::While { body, .. } => Some(self.flow_loop(facts, body, None)),
269 Stmt::For(f) => Some(self.flow_loop(facts, &f.body, Some(&f.var))),
270 Stmt::Match { arms, .. } => {
271 let mut after = facts.clone();
275 for arm in arms {
276 let _ = self.block(facts.clone(), &arm.body);
277 self.scan_invalidations(&mut after, &arm.body);
278 }
279 Some(after)
280 }
281 }
282 }
283
284 fn after_expr_stmt(&self, mut facts: FlowFacts, e: ExprId) -> FlowFacts {
287 if let Expr::Bin {
288 op: BinOp::Assign,
289 lhs,
290 ..
291 } = self.body.expr(e)
292 && let Some(p) = Place::of(self.body, *lhs)
293 {
294 facts.invalidate_assigned(&p);
295 }
296 if self.expr_contains_call(e) {
297 facts.invalidate_self_rooted();
298 }
299 facts
300 }
301
302 fn flow_if(
306 &mut self,
307 facts: &FlowFacts,
308 cond: ExprId,
309 then_branch: &[StmtId],
310 elifs: &[(ExprId, crate::body::Block)],
311 else_branch: Option<&[StmtId]>,
312 ) -> Option<FlowFacts> {
313 let mut exits: Vec<Option<FlowFacts>> = Vec::new();
314 let then_in = self.apply(facts, cond, true);
315 exits.push(self.block(then_in, then_branch));
316
317 let mut chain = self.apply(facts, cond, false);
319 for (econd, eblock) in elifs {
320 let etrue = self.apply(&chain, *econd, true);
321 exits.push(self.block(etrue, eblock));
322 chain = self.apply(&chain, *econd, false);
323 }
324 exits.push(match else_branch {
326 Some(eb) => self.block(chain, eb),
327 None => Some(chain),
328 });
329
330 join_exits(exits)
331 }
332
333 fn flow_loop(
337 &mut self,
338 facts: FlowFacts,
339 body: &[StmtId],
340 loop_var: Option<&SmolStr>,
341 ) -> FlowFacts {
342 let mut widened = facts;
343 if let Some(v) = loop_var {
344 widened.invalidate_assigned(&Place::Local(v.clone()));
345 }
346 self.scan_invalidations(&mut widened, body);
347 let _ = self.block(widened.clone(), body);
350 widened
351 }
352
353 fn apply(&self, facts: &FlowFacts, cond: ExprId, truthy: bool) -> FlowFacts {
355 let mut out = facts.clone();
356 for (p, t) in self.derive_facts(cond, truthy) {
357 out.insert(p, t);
358 }
359 if self.expr_contains_call(cond) {
364 out.invalidate_self_rooted();
365 }
366 out
367 }
368
369 fn derive_facts(&self, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
371 match self.body.expr(cond) {
372 Expr::Paren(inner) => self.derive_facts(*inner, truthy),
373 Expr::Unary {
374 op: UnOp::Not,
375 operand,
376 } => self.derive_facts(*operand, !truthy),
377 Expr::Is {
378 operand,
379 ty: Some(ptr),
380 negated,
381 } => {
382 let positive = truthy != *negated;
383 Place::of(self.body, *operand)
384 .map(|p| {
385 let t = if positive {
386 NarrowedTy::Is(*ptr)
387 } else {
388 NarrowedTy::Not(*ptr)
389 };
390 vec![(p, t)]
391 })
392 .unwrap_or_default()
393 }
394 Expr::Bin {
395 op: BinOp::Eq,
396 lhs,
397 rhs,
398 } => self.null_cmp_facts(*lhs, *rhs, true, truthy),
399 Expr::Bin {
400 op: BinOp::Ne,
401 lhs,
402 rhs,
403 } => self.null_cmp_facts(*lhs, *rhs, false, truthy),
404 Expr::Bin {
407 op: BinOp::And,
408 lhs,
409 rhs,
410 } if truthy => {
411 let mut v = self.derive_facts(*lhs, true);
412 v.extend(self.derive_facts(*rhs, true));
413 v
414 }
415 Expr::Bin {
416 op: BinOp::Or,
417 lhs,
418 rhs,
419 } if !truthy => {
420 let mut v = self.derive_facts(*lhs, false);
421 v.extend(self.derive_facts(*rhs, false));
422 v
423 }
424 _ if truthy => Place::of(self.body, cond)
426 .map(|p| vec![(p, NarrowedTy::NotNull)])
427 .unwrap_or_default(),
428 _ => Vec::new(),
429 }
430 }
431
432 fn null_cmp_facts(
435 &self,
436 lhs: ExprId,
437 rhs: ExprId,
438 is_eq: bool,
439 truthy: bool,
440 ) -> Vec<(Place, NarrowedTy)> {
441 let other = if self.is_null(lhs) {
442 rhs
443 } else if self.is_null(rhs) {
444 lhs
445 } else {
446 return Vec::new();
447 };
448 let proves_not_null = if is_eq { !truthy } else { truthy };
449 if proves_not_null {
450 Place::of(self.body, other)
451 .map(|p| vec![(p, NarrowedTy::NotNull)])
452 .unwrap_or_default()
453 } else {
454 Vec::new()
455 }
456 }
457
458 fn is_null(&self, id: ExprId) -> bool {
459 matches!(self.body.expr(id), Expr::Literal(Literal::Null))
460 }
461
462 fn scan_invalidations(&self, facts: &mut FlowFacts, block: &[StmtId]) {
465 for &sid in block {
466 match self.body.stmt(sid) {
467 Stmt::Expr(e) => {
468 if let Expr::Bin {
469 op: BinOp::Assign,
470 lhs,
471 ..
472 } = self.body.expr(*e)
473 && let Some(p) = Place::of(self.body, *lhs)
474 {
475 facts.invalidate_assigned(&p);
476 }
477 if self.expr_contains_call(*e) {
478 facts.invalidate_self_rooted();
479 }
480 }
481 Stmt::Var(v) => facts.invalidate_assigned(&Place::Local(v.name.clone())),
482 Stmt::If {
483 cond,
484 then_branch,
485 elifs,
486 else_branch,
487 } => {
488 if self.expr_contains_call(*cond) {
491 facts.invalidate_self_rooted();
492 }
493 self.scan_invalidations(facts, then_branch);
494 for (econd, b) in elifs {
495 if self.expr_contains_call(*econd) {
496 facts.invalidate_self_rooted();
497 }
498 self.scan_invalidations(facts, b);
499 }
500 if let Some(eb) = else_branch {
501 self.scan_invalidations(facts, eb);
502 }
503 }
504 Stmt::While { cond, body } => {
505 if self.expr_contains_call(*cond) {
506 facts.invalidate_self_rooted();
507 }
508 self.scan_invalidations(facts, body);
509 }
510 Stmt::For(f) => {
511 facts.invalidate_assigned(&Place::Local(f.var.clone()));
512 if self.expr_contains_call(f.iter) {
513 facts.invalidate_self_rooted();
514 }
515 self.scan_invalidations(facts, &f.body);
516 }
517 Stmt::Match { scrutinee, arms } => {
518 if self.expr_contains_call(*scrutinee) {
519 facts.invalidate_self_rooted();
520 }
521 for arm in arms {
522 self.scan_invalidations(facts, &arm.body);
523 }
524 }
525 Stmt::Assert(Some(c)) => {
526 if self.expr_contains_call(*c) {
527 facts.invalidate_self_rooted();
528 }
529 }
530 Stmt::Return(_)
531 | Stmt::Break
532 | Stmt::Continue
533 | Stmt::Pass
534 | Stmt::Assert(None) => {}
535 }
536 }
537 }
538
539 fn expr_contains_call(&self, id: ExprId) -> bool {
541 match self.body.expr(id) {
542 Expr::Call { .. } => true,
543 Expr::Bin { lhs, rhs, .. } | Expr::In { lhs, rhs, .. } => {
544 self.expr_contains_call(*lhs) || self.expr_contains_call(*rhs)
545 }
546 Expr::Unary { operand, .. }
547 | Expr::Await(operand)
548 | Expr::Paren(operand)
549 | Expr::Cast { operand, .. }
550 | Expr::Is { operand, .. } => self.expr_contains_call(*operand),
551 Expr::Ternary {
552 cond,
553 then_branch,
554 else_branch,
555 } => {
556 self.expr_contains_call(*cond)
557 || self.expr_contains_call(*then_branch)
558 || self.expr_contains_call(*else_branch)
559 }
560 Expr::Field { receiver, .. } => self.expr_contains_call(*receiver),
561 Expr::Index { base, index } => {
562 self.expr_contains_call(*base) || self.expr_contains_call(*index)
563 }
564 Expr::Array(items) => items.iter().any(|&e| self.expr_contains_call(e)),
565 Expr::Dict(entries) => entries.iter().any(|(k, v)| {
566 self.expr_contains_call(*k) || v.is_some_and(|e| self.expr_contains_call(e))
567 }),
568 _ => false,
569 }
570 }
571}
572
573#[must_use]
577pub fn condition_facts(body: &Body, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
578 Analyzer {
579 body,
580 entry_facts: FxHashMap::default(),
581 unreachable_anchors: Vec::new(),
582 }
583 .derive_facts(cond, truthy)
584}
585
586fn join_exits(exits: Vec<Option<FlowFacts>>) -> Option<FlowFacts> {
589 let mut iter = exits.into_iter().flatten();
590 let first = iter.next()?;
591 Some(iter.fold(first, |acc, f| acc.join(&f)))
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::body::{self, Body};
598 use gdscript_syntax::{SyntaxKind, ast, parse};
599
600 fn func_body(src: &str) -> Body {
601 let root = parse(src).syntax_node();
602 let func = ast::descendants(&root)
603 .into_iter()
604 .find(|n| n.kind() == SyntaxKind::FuncDecl)
605 .expect("a FuncDecl");
606 body::body_of_func(&func)
607 }
608
609 fn fact_at(body: &Body, a: &FlowAnalysis, i: usize) -> Option<(Place, NarrowedTy)> {
611 let sid = body.block[i];
612 let facts = a.facts_before(sid)?;
613 facts.0.iter().next().map(|(p, t)| (p.clone(), t.clone()))
614 }
615
616 #[test]
617 fn is_guard_narrows_then_branch() {
618 let body = func_body("func f(x):\n\tif x is Node:\n\t\tx.free()\n");
619 let a = analyze(&body);
620 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
622 panic!("if")
623 };
624 let inner = a.facts_before(then_branch[0]).expect("then facts");
625 assert_eq!(
626 inner.get(&Place::Local("x".into())),
627 Some(&NarrowedTy::Is(match body.stmt(body.block[0]) {
628 Stmt::If { cond, .. } => match body.expr(*cond) {
629 Expr::Is { ty: Some(p), .. } => *p,
630 _ => panic!("is"),
631 },
632 _ => unreachable!(),
633 })),
634 );
635 }
636
637 #[test]
638 fn early_return_narrows_after_the_guard() {
639 let body = func_body("func f(x):\n\tif x == null:\n\t\treturn\n\tx.free()\n");
641 let a = analyze(&body);
642 let after = a.facts_before(body.block[1]).expect("after-if facts");
644 assert_eq!(
645 after.get(&Place::Local("x".into())),
646 Some(&NarrowedTy::NotNull)
647 );
648 }
649
650 #[test]
651 fn code_after_return_is_unreachable() {
652 let body = func_body("func f():\n\treturn\n\tvar dead := 1\n");
653 let a = analyze(&body);
654 assert_eq!(a.unreachable_ranges(&body).len(), 1);
655 assert_eq!(a.unreachable_anchors, vec![body.block[1]]);
657 }
658
659 #[test]
660 fn reassignment_invalidates_narrowing() {
661 let body = func_body("func f(x, other):\n\tif x is Node:\n\t\tx = other\n\t\tx.free()\n");
663 let a = analyze(&body);
664 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
665 panic!("if")
666 };
667 let at_free = a.facts_before(then_branch[1]).expect("facts");
669 assert_eq!(at_free.get(&Place::Local("x".into())), None);
670 }
671
672 #[test]
673 fn opaque_call_invalidates_self_members() {
674 let body =
676 func_body("func f():\n\tif self.node is Node2D:\n\t\tmutate()\n\t\tself.node.foo()\n");
677 let a = analyze(&body);
678 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
679 panic!("if")
680 };
681 let at_use = a.facts_before(then_branch[1]).expect("facts");
682 assert_eq!(at_use.get(&Place::SelfMember("node".into())), None);
683 }
684
685 #[test]
686 fn opaque_call_in_guard_invalidates_self_member_narrowing() {
687 let body =
690 func_body("func f():\n\tif self.node is Node2D and mutate():\n\t\tself.node.foo()\n");
691 let a = analyze(&body);
692 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
693 panic!("if")
694 };
695 let inner = a.facts_before(then_branch[0]).expect("then facts");
696 assert_eq!(inner.get(&Place::SelfMember("node".into())), None);
697 }
698
699 #[test]
700 fn merge_drops_disagreeing_facts() {
701 let body =
703 func_body("func f(x):\n\tif x is Node:\n\t\tpass\n\telse:\n\t\tpass\n\tx.free()\n");
704 let a = analyze(&body);
705 let after = fact_at(&body, &a, 1);
706 assert!(
707 after.is_none(),
708 "narrowing must not survive a non-exhaustive merge"
709 );
710 }
711
712 #[test]
713 fn and_short_circuit_narrows_rhs_and_after() {
714 let body = func_body("func f(x):\n\tif x is Node and true:\n\t\tx.free()\n");
716 let a = analyze(&body);
717 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
718 panic!("if")
719 };
720 let inner = a.facts_before(then_branch[0]).expect("then facts");
721 assert!(matches!(
722 inner.get(&Place::Local("x".into())),
723 Some(NarrowedTy::Is(_))
724 ));
725 }
726
727 #[test]
728 fn loop_body_is_entered_widened() {
729 let body = func_body(
731 "func f(x, other):\n\tif x is Node:\n\t\twhile true:\n\t\t\tx = other\n\t\t\tx.free()\n",
732 );
733 let a = analyze(&body);
734 let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
736 panic!("if")
737 };
738 assert!(a.facts_before(then_branch[0]).is_some());
739 }
740}