use rustc_hash::FxHashMap;
use smol_str::SmolStr;
use gdscript_base::TextRange;
use crate::body::{BinOp, Body, Expr, ExprId, Literal, Stmt, StmtId, UnOp};
use crate::cst::AstPtr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Place {
Local(SmolStr),
SelfMember(SmolStr),
Field(Box<Place>, SmolStr),
}
impl Place {
#[must_use]
pub fn of(body: &Body, id: ExprId) -> Option<Place> {
match body.expr(id) {
Expr::Name(n) => Some(Place::Local(n.clone())),
Expr::Paren(inner) => Place::of(body, *inner),
Expr::Field { receiver, name, .. } => match body.expr(*receiver) {
Expr::SelfExpr => Some(Place::SelfMember(name.clone())),
_ => Some(Place::Field(
Box::new(Place::of(body, *receiver)?),
name.clone(),
)),
},
_ => None,
}
}
#[must_use]
pub fn invalidated_by(&self, assigned: &Place) -> bool {
let mut cur = self;
loop {
if cur == assigned {
return true;
}
match cur {
Place::Field(base, _) => cur = base,
_ => return false,
}
}
}
#[must_use]
pub fn dotted_key(&self) -> String {
match self {
Place::Local(n) => n.to_string(),
Place::SelfMember(m) => format!("self.{m}"),
Place::Field(base, name) => format!("{}.{name}", base.dotted_key()),
}
}
#[must_use]
fn is_self_rooted(&self) -> bool {
match self {
Place::SelfMember(_) => true,
Place::Field(base, _) => base.is_self_rooted(),
Place::Local(_) => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NarrowedTy {
Is(AstPtr),
NotNull,
Not(AstPtr),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FlowFacts(FxHashMap<Place, NarrowedTy>);
impl FlowFacts {
#[must_use]
pub fn get(&self, place: &Place) -> Option<&NarrowedTy> {
self.0.get(place)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&Place, &NarrowedTy)> {
self.0.iter()
}
fn insert(&mut self, place: Place, ty: NarrowedTy) {
if matches!(ty, NarrowedTy::NotNull)
&& matches!(self.0.get(&place), Some(NarrowedTy::Is(_)))
{
return;
}
self.0.insert(place, ty);
}
fn invalidate_assigned(&mut self, assigned: &Place) {
self.0.retain(|p, _| !p.invalidated_by(assigned));
}
fn invalidate_self_rooted(&mut self) {
self.0.retain(|p, _| !p.is_self_rooted());
}
#[must_use]
fn join(&self, other: &FlowFacts) -> FlowFacts {
let mut out = FxHashMap::default();
for (p, t) in &self.0 {
if other.0.get(p) == Some(t) {
out.insert(p.clone(), t.clone());
}
}
FlowFacts(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct FlowAnalysis {
entry_facts: FxHashMap<StmtId, FlowFacts>,
unreachable_anchors: Vec<StmtId>,
}
impl FlowAnalysis {
#[must_use]
pub fn facts_before(&self, stmt: StmtId) -> Option<&FlowFacts> {
self.entry_facts.get(&stmt)
}
#[must_use]
pub fn unreachable_ranges(&self, body: &Body) -> Vec<TextRange> {
self.unreachable_anchors
.iter()
.map(|&sid| body.source_map.stmt_range(sid))
.collect()
}
}
#[must_use]
pub fn analyze(body: &Body) -> FlowAnalysis {
let mut a = Analyzer {
body,
entry_facts: FxHashMap::default(),
unreachable_anchors: Vec::new(),
};
a.block(FlowFacts::default(), &body.block);
for expr in &body.exprs {
if let Expr::Lambda { body: lbody, .. } = expr {
a.block(FlowFacts::default(), lbody);
}
}
FlowAnalysis {
entry_facts: a.entry_facts,
unreachable_anchors: a.unreachable_anchors,
}
}
struct Analyzer<'a> {
body: &'a Body,
entry_facts: FxHashMap<StmtId, FlowFacts>,
unreachable_anchors: Vec<StmtId>,
}
impl Analyzer<'_> {
fn block(&mut self, facts: FlowFacts, block: &[StmtId]) -> Option<FlowFacts> {
let mut cur = Some(facts);
for &sid in block {
let Some(f) = cur else {
self.unreachable_anchors.push(sid);
return None;
};
cur = self.stmt(f, sid);
}
cur
}
fn stmt(&mut self, facts: FlowFacts, sid: StmtId) -> Option<FlowFacts> {
self.entry_facts.insert(sid, facts.clone());
match self.body.stmt(sid) {
Stmt::Return(_) | Stmt::Break | Stmt::Continue => None,
Stmt::Pass | Stmt::Assert(_) => Some(facts),
Stmt::Expr(e) => Some(self.after_expr_stmt(facts, *e)),
Stmt::Var(v) => {
let mut f = facts;
f.invalidate_assigned(&Place::Local(v.name.clone()));
Some(f)
}
Stmt::If {
cond,
then_branch,
elifs,
else_branch,
} => self.flow_if(&facts, *cond, then_branch, elifs, else_branch.as_deref()),
Stmt::While { body, .. } => Some(self.flow_loop(facts, body, None)),
Stmt::For(f) => Some(self.flow_loop(facts, &f.body, Some(&f.var))),
Stmt::Match { arms, .. } => {
let mut after = facts.clone();
for arm in arms {
let _ = self.block(facts.clone(), &arm.body);
self.scan_invalidations(&mut after, &arm.body);
}
Some(after)
}
}
}
fn after_expr_stmt(&self, mut facts: FlowFacts, e: ExprId) -> FlowFacts {
if let Expr::Bin {
op: BinOp::Assign,
lhs,
..
} = self.body.expr(e)
&& let Some(p) = Place::of(self.body, *lhs)
{
facts.invalidate_assigned(&p);
}
if self.expr_contains_call(e) {
facts.invalidate_self_rooted();
}
facts
}
fn flow_if(
&mut self,
facts: &FlowFacts,
cond: ExprId,
then_branch: &[StmtId],
elifs: &[(ExprId, crate::body::Block)],
else_branch: Option<&[StmtId]>,
) -> Option<FlowFacts> {
let mut exits: Vec<Option<FlowFacts>> = Vec::new();
let then_in = self.apply(facts, cond, true);
exits.push(self.block(then_in, then_branch));
let mut chain = self.apply(facts, cond, false);
for (econd, eblock) in elifs {
let etrue = self.apply(&chain, *econd, true);
exits.push(self.block(etrue, eblock));
chain = self.apply(&chain, *econd, false);
}
exits.push(match else_branch {
Some(eb) => self.block(chain, eb),
None => Some(chain),
});
join_exits(exits)
}
fn flow_loop(
&mut self,
facts: FlowFacts,
body: &[StmtId],
loop_var: Option<&SmolStr>,
) -> FlowFacts {
let mut widened = facts;
if let Some(v) = loop_var {
widened.invalidate_assigned(&Place::Local(v.clone()));
}
self.scan_invalidations(&mut widened, body);
let _ = self.block(widened.clone(), body);
widened
}
fn apply(&self, facts: &FlowFacts, cond: ExprId, truthy: bool) -> FlowFacts {
let mut out = facts.clone();
for (p, t) in self.derive_facts(cond, truthy) {
out.insert(p, t);
}
if self.expr_contains_call(cond) {
out.invalidate_self_rooted();
}
out
}
fn derive_facts(&self, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
match self.body.expr(cond) {
Expr::Paren(inner) => self.derive_facts(*inner, truthy),
Expr::Unary {
op: UnOp::Not,
operand,
} => self.derive_facts(*operand, !truthy),
Expr::Is {
operand,
ty: Some(ptr),
negated,
} => {
let positive = truthy != *negated;
Place::of(self.body, *operand)
.map(|p| {
let t = if positive {
NarrowedTy::Is(*ptr)
} else {
NarrowedTy::Not(*ptr)
};
vec![(p, t)]
})
.unwrap_or_default()
}
Expr::Bin {
op: BinOp::Eq,
lhs,
rhs,
} => self.null_cmp_facts(*lhs, *rhs, true, truthy),
Expr::Bin {
op: BinOp::Ne,
lhs,
rhs,
} => self.null_cmp_facts(*lhs, *rhs, false, truthy),
Expr::Bin {
op: BinOp::And,
lhs,
rhs,
} if truthy => {
let mut v = self.derive_facts(*lhs, true);
v.extend(self.derive_facts(*rhs, true));
v
}
Expr::Bin {
op: BinOp::Or,
lhs,
rhs,
} if !truthy => {
let mut v = self.derive_facts(*lhs, false);
v.extend(self.derive_facts(*rhs, false));
v
}
_ if truthy => Place::of(self.body, cond)
.map(|p| vec![(p, NarrowedTy::NotNull)])
.unwrap_or_default(),
_ => Vec::new(),
}
}
fn null_cmp_facts(
&self,
lhs: ExprId,
rhs: ExprId,
is_eq: bool,
truthy: bool,
) -> Vec<(Place, NarrowedTy)> {
let other = if self.is_null(lhs) {
rhs
} else if self.is_null(rhs) {
lhs
} else {
return Vec::new();
};
let proves_not_null = if is_eq { !truthy } else { truthy };
if proves_not_null {
Place::of(self.body, other)
.map(|p| vec![(p, NarrowedTy::NotNull)])
.unwrap_or_default()
} else {
Vec::new()
}
}
fn is_null(&self, id: ExprId) -> bool {
matches!(self.body.expr(id), Expr::Literal(Literal::Null))
}
fn scan_invalidations(&self, facts: &mut FlowFacts, block: &[StmtId]) {
for &sid in block {
match self.body.stmt(sid) {
Stmt::Expr(e) => {
if let Expr::Bin {
op: BinOp::Assign,
lhs,
..
} = self.body.expr(*e)
&& let Some(p) = Place::of(self.body, *lhs)
{
facts.invalidate_assigned(&p);
}
if self.expr_contains_call(*e) {
facts.invalidate_self_rooted();
}
}
Stmt::Var(v) => facts.invalidate_assigned(&Place::Local(v.name.clone())),
Stmt::If {
cond,
then_branch,
elifs,
else_branch,
} => {
if self.expr_contains_call(*cond) {
facts.invalidate_self_rooted();
}
self.scan_invalidations(facts, then_branch);
for (econd, b) in elifs {
if self.expr_contains_call(*econd) {
facts.invalidate_self_rooted();
}
self.scan_invalidations(facts, b);
}
if let Some(eb) = else_branch {
self.scan_invalidations(facts, eb);
}
}
Stmt::While { cond, body } => {
if self.expr_contains_call(*cond) {
facts.invalidate_self_rooted();
}
self.scan_invalidations(facts, body);
}
Stmt::For(f) => {
facts.invalidate_assigned(&Place::Local(f.var.clone()));
if self.expr_contains_call(f.iter) {
facts.invalidate_self_rooted();
}
self.scan_invalidations(facts, &f.body);
}
Stmt::Match { scrutinee, arms } => {
if self.expr_contains_call(*scrutinee) {
facts.invalidate_self_rooted();
}
for arm in arms {
self.scan_invalidations(facts, &arm.body);
}
}
Stmt::Assert(Some(c)) => {
if self.expr_contains_call(*c) {
facts.invalidate_self_rooted();
}
}
Stmt::Return(_)
| Stmt::Break
| Stmt::Continue
| Stmt::Pass
| Stmt::Assert(None) => {}
}
}
}
fn expr_contains_call(&self, id: ExprId) -> bool {
match self.body.expr(id) {
Expr::Call { .. } => true,
Expr::Bin { lhs, rhs, .. } | Expr::In { lhs, rhs, .. } => {
self.expr_contains_call(*lhs) || self.expr_contains_call(*rhs)
}
Expr::Unary { operand, .. }
| Expr::Await(operand)
| Expr::Paren(operand)
| Expr::Cast { operand, .. }
| Expr::Is { operand, .. } => self.expr_contains_call(*operand),
Expr::Ternary {
cond,
then_branch,
else_branch,
} => {
self.expr_contains_call(*cond)
|| self.expr_contains_call(*then_branch)
|| self.expr_contains_call(*else_branch)
}
Expr::Field { receiver, .. } => self.expr_contains_call(*receiver),
Expr::Index { base, index } => {
self.expr_contains_call(*base) || self.expr_contains_call(*index)
}
Expr::Array(items) => items.iter().any(|&e| self.expr_contains_call(e)),
Expr::Dict(entries) => entries.iter().any(|(k, v)| {
self.expr_contains_call(*k) || v.is_some_and(|e| self.expr_contains_call(e))
}),
_ => false,
}
}
}
#[must_use]
pub fn condition_facts(body: &Body, cond: ExprId, truthy: bool) -> Vec<(Place, NarrowedTy)> {
Analyzer {
body,
entry_facts: FxHashMap::default(),
unreachable_anchors: Vec::new(),
}
.derive_facts(cond, truthy)
}
fn join_exits(exits: Vec<Option<FlowFacts>>) -> Option<FlowFacts> {
let mut iter = exits.into_iter().flatten();
let first = iter.next()?;
Some(iter.fold(first, |acc, f| acc.join(&f)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::{self, Body};
use gdscript_syntax::{SyntaxKind, ast, parse};
fn func_body(src: &str) -> Body {
let root = parse(src).syntax_node();
let func = ast::descendants(&root)
.into_iter()
.find(|n| n.kind() == SyntaxKind::FuncDecl)
.expect("a FuncDecl");
body::body_of_func(&func)
}
fn fact_at(body: &Body, a: &FlowAnalysis, i: usize) -> Option<(Place, NarrowedTy)> {
let sid = body.block[i];
let facts = a.facts_before(sid)?;
facts.0.iter().next().map(|(p, t)| (p.clone(), t.clone()))
}
#[test]
fn is_guard_narrows_then_branch() {
let body = func_body("func f(x):\n\tif x is Node:\n\t\tx.free()\n");
let a = analyze(&body);
let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
panic!("if")
};
let inner = a.facts_before(then_branch[0]).expect("then facts");
assert_eq!(
inner.get(&Place::Local("x".into())),
Some(&NarrowedTy::Is(match body.stmt(body.block[0]) {
Stmt::If { cond, .. } => match body.expr(*cond) {
Expr::Is { ty: Some(p), .. } => *p,
_ => panic!("is"),
},
_ => unreachable!(),
})),
);
}
#[test]
fn early_return_narrows_after_the_guard() {
let body = func_body("func f(x):\n\tif x == null:\n\t\treturn\n\tx.free()\n");
let a = analyze(&body);
let after = a.facts_before(body.block[1]).expect("after-if facts");
assert_eq!(
after.get(&Place::Local("x".into())),
Some(&NarrowedTy::NotNull)
);
}
#[test]
fn code_after_return_is_unreachable() {
let body = func_body("func f():\n\treturn\n\tvar dead := 1\n");
let a = analyze(&body);
assert_eq!(a.unreachable_ranges(&body).len(), 1);
assert_eq!(a.unreachable_anchors, vec![body.block[1]]);
}
#[test]
fn reassignment_invalidates_narrowing() {
let body = func_body("func f(x, other):\n\tif x is Node:\n\t\tx = other\n\t\tx.free()\n");
let a = analyze(&body);
let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
panic!("if")
};
let at_free = a.facts_before(then_branch[1]).expect("facts");
assert_eq!(at_free.get(&Place::Local("x".into())), None);
}
#[test]
fn opaque_call_invalidates_self_members() {
let body =
func_body("func f():\n\tif self.node is Node2D:\n\t\tmutate()\n\t\tself.node.foo()\n");
let a = analyze(&body);
let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
panic!("if")
};
let at_use = a.facts_before(then_branch[1]).expect("facts");
assert_eq!(at_use.get(&Place::SelfMember("node".into())), None);
}
#[test]
fn opaque_call_in_guard_invalidates_self_member_narrowing() {
let body =
func_body("func f():\n\tif self.node is Node2D and mutate():\n\t\tself.node.foo()\n");
let a = analyze(&body);
let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
panic!("if")
};
let inner = a.facts_before(then_branch[0]).expect("then facts");
assert_eq!(inner.get(&Place::SelfMember("node".into())), None);
}
#[test]
fn merge_drops_disagreeing_facts() {
let body =
func_body("func f(x):\n\tif x is Node:\n\t\tpass\n\telse:\n\t\tpass\n\tx.free()\n");
let a = analyze(&body);
let after = fact_at(&body, &a, 1);
assert!(
after.is_none(),
"narrowing must not survive a non-exhaustive merge"
);
}
#[test]
fn and_short_circuit_narrows_rhs_and_after() {
let body = func_body("func f(x):\n\tif x is Node and true:\n\t\tx.free()\n");
let a = analyze(&body);
let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
panic!("if")
};
let inner = a.facts_before(then_branch[0]).expect("then facts");
assert!(matches!(
inner.get(&Place::Local("x".into())),
Some(NarrowedTy::Is(_))
));
}
#[test]
fn loop_body_is_entered_widened() {
let body = func_body(
"func f(x, other):\n\tif x is Node:\n\t\twhile true:\n\t\t\tx = other\n\t\t\tx.free()\n",
);
let a = analyze(&body);
let Stmt::If { then_branch, .. } = body.stmt(body.block[0]) else {
panic!("if")
};
assert!(a.facts_before(then_branch[0]).is_some());
}
}