use gdscript_base::TextRange;
use gdscript_syntax::ast::{self, AstNode};
use gdscript_syntax::{GdNode, SyntaxKind};
use smol_str::SmolStr;
use crate::cst::{self, AstPtr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExprId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StmtId(pub u32);
pub type Block = Vec<StmtId>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Literal {
Int,
Float,
Bool,
Str,
StringName,
NodePath,
Null,
MathConst,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinOp {
Add,
Sub,
Mul,
Div,
Mod,
Pow,
Eq,
Ne,
Lt,
Gt,
Le,
Ge,
And,
Or,
BitAnd,
BitOr,
BitXor,
Shl,
Shr,
Assign,
}
impl BinOp {
#[must_use]
pub fn from_token(kind: SyntaxKind) -> Option<Self> {
use SyntaxKind as K;
Some(match kind {
K::Plus => Self::Add,
K::Minus => Self::Sub,
K::Star => Self::Mul,
K::Slash => Self::Div,
K::Percent => Self::Mod,
K::StarStar => Self::Pow,
K::EqEq => Self::Eq,
K::Neq => Self::Ne,
K::Lt => Self::Lt,
K::Gt => Self::Gt,
K::Le => Self::Le,
K::Ge => Self::Ge,
K::AndKw | K::AmpAmp => Self::And,
K::OrKw | K::PipePipe => Self::Or,
K::Amp => Self::BitAnd,
K::Pipe => Self::BitOr,
K::Caret => Self::BitXor,
K::Shl => Self::Shl,
K::Shr => Self::Shr,
K::Eq
| K::PlusEq
| K::MinusEq
| K::StarEq
| K::SlashEq
| K::PercentEq
| K::StarStarEq
| K::AmpEq
| K::PipeEq
| K::CaretEq
| K::ShlEq
| K::ShrEq => Self::Assign,
_ => return None,
})
}
#[must_use]
pub fn is_arithmetic(self) -> bool {
matches!(
self,
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod | Self::Pow
)
}
#[must_use]
pub fn is_boolean(self) -> bool {
matches!(
self,
Self::Eq | Self::Ne | Self::Lt | Self::Gt | Self::Le | Self::Ge | Self::And | Self::Or
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnOp {
Neg,
Pos,
Not,
BitNot,
}
impl UnOp {
#[must_use]
pub fn from_token(kind: SyntaxKind) -> Option<Self> {
Some(match kind {
SyntaxKind::Minus => Self::Neg,
SyntaxKind::Plus => Self::Pos,
SyntaxKind::NotKw | SyntaxKind::Bang => Self::Not,
SyntaxKind::Tilde => Self::BitNot,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Missing,
Literal(Literal),
Name(SmolStr),
SelfExpr,
Super,
Bin {
op: BinOp,
lhs: ExprId,
rhs: ExprId,
},
Unary {
op: UnOp,
operand: ExprId,
},
Ternary {
cond: ExprId,
then_branch: ExprId,
else_branch: ExprId,
},
Call {
callee: ExprId,
args: Vec<ExprId>,
},
Field {
receiver: ExprId,
name: SmolStr,
name_range: TextRange,
},
Index {
base: ExprId,
index: ExprId,
},
Is {
operand: ExprId,
ty: Option<AstPtr>,
negated: bool,
},
Cast {
operand: ExprId,
ty: Option<AstPtr>,
},
In {
lhs: ExprId,
rhs: ExprId,
negated: bool,
},
Await(ExprId),
Array(Vec<ExprId>),
Dict(Vec<(ExprId, Option<ExprId>)>),
Lambda {
params: Vec<ParamBinding>,
body: Block,
},
Preload {
arg: Option<ExprId>,
path: Option<SmolStr>,
},
GetNode {
path: Option<SmolStr>,
unique: bool,
},
Paren(ExprId),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamBinding {
pub name: SmolStr,
pub type_ref: Option<AstPtr>,
pub default: Option<ExprId>,
pub name_range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalVar {
pub name: SmolStr,
pub type_ref: Option<AstPtr>,
pub init: Option<ExprId>,
pub is_inferred: bool,
pub is_const: bool,
pub name_range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForLoop {
pub var: SmolStr,
pub var_type: Option<AstPtr>,
pub var_range: TextRange,
pub iter: ExprId,
pub body: Block,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchBind {
pub name: SmolStr,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchArm {
pub binds: Vec<MatchBind>,
pub guard: Option<ExprId>,
pub body: Block,
pub range: TextRange,
pub is_catch_all: bool,
}
fn arm_is_unconditional_catch_all(arm: &GdNode) -> bool {
use SyntaxKind as K;
if cst::first_child(arm, |k| k == K::PatternGuard).is_some() {
return false;
}
let patterns: Vec<&GdNode> = arm
.children()
.filter(|c| {
matches!(
c.kind(),
K::PatternBind
| K::PatternLiteral
| K::PatternWildcard
| K::PatternArray
| K::PatternDict
| K::PatternRest
)
})
.collect();
let [only] = patterns.as_slice() else {
return false;
};
match only.kind() {
K::PatternBind | K::PatternWildcard => true,
K::PatternLiteral => cst::first_child_expr(only)
.and_then(|e| cst::first_token(&e))
.is_some_and(|t| t.text() == "_"),
_ => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stmt {
Expr(ExprId),
Var(LocalVar),
Return(Option<ExprId>),
If {
cond: ExprId,
then_branch: Block,
elifs: Vec<(ExprId, Block)>,
else_branch: Option<Block>,
},
While {
cond: ExprId,
body: Block,
},
For(ForLoop),
Match {
scrutinee: ExprId,
arms: Vec<MatchArm>,
},
Break,
Continue,
Pass,
Assert(Option<ExprId>),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BodySourceMap {
expr_ranges: Vec<TextRange>,
stmt_ranges: Vec<TextRange>,
}
impl BodySourceMap {
#[must_use]
pub fn expr_range(&self, id: ExprId) -> TextRange {
self.expr_ranges[id.0 as usize]
}
#[must_use]
pub fn stmt_range(&self, id: StmtId) -> TextRange {
self.stmt_ranges[id.0 as usize]
}
#[must_use]
pub fn expr_at_offset(&self, offset: u32) -> Option<ExprId> {
self.expr_ranges
.iter()
.enumerate()
.filter(|(_, r)| r.start <= offset && offset < r.end)
.min_by_key(|(_, r)| r.end - r.start)
.map(|(i, _)| ExprId(u32::try_from(i).unwrap_or(u32::MAX)))
}
#[must_use]
pub fn expr_for_range(&self, range: TextRange) -> Option<ExprId> {
self.expr_ranges
.iter()
.position(|r| *r == range)
.map(|i| ExprId(u32::try_from(i).unwrap_or(u32::MAX)))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Body {
pub exprs: Vec<Expr>,
pub stmts: Vec<Stmt>,
pub params: Vec<ParamBinding>,
pub block: Block,
pub tail: Option<ExprId>,
pub source_map: BodySourceMap,
}
impl Body {
#[must_use]
pub fn expr(&self, id: ExprId) -> &Expr {
&self.exprs[id.0 as usize]
}
#[must_use]
pub fn stmt(&self, id: StmtId) -> &Stmt {
&self.stmts[id.0 as usize]
}
}
#[must_use]
pub fn body_of_func(func: &GdNode) -> Body {
let mut low = Lowerer::default();
let decl = ast::FuncDecl::cast(func.clone());
let params = decl
.as_ref()
.and_then(ast::FuncDecl::param_list)
.map(|pl| low.lower_params(pl.syntax()))
.unwrap_or_default();
let block = decl
.as_ref()
.and_then(ast::FuncDecl::body)
.map(|b| low.lower_block(b.syntax()))
.unwrap_or_default();
low.finish(params, block, None)
}
#[must_use]
pub fn body_of_expr(expr: &GdNode) -> Body {
let mut low = Lowerer::default();
let tail = low.lower_expr(expr);
low.finish(Vec::new(), Vec::new(), Some(tail))
}
#[must_use]
pub fn body_of_decl_stmt(decl: &GdNode) -> Body {
let mut low = Lowerer::default();
let block = low.lower_stmt(decl).into_iter().collect();
low.finish(Vec::new(), block, None)
}
#[must_use]
pub fn body(root: &GdNode, ptr: AstPtr) -> Option<Body> {
let node = ptr.to_node(root)?;
Some(body_of_func(&node))
}
#[derive(Default)]
struct Lowerer {
exprs: Vec<Expr>,
stmts: Vec<Stmt>,
expr_ranges: Vec<TextRange>,
stmt_ranges: Vec<TextRange>,
}
impl Lowerer {
fn finish(self, params: Vec<ParamBinding>, block: Block, tail: Option<ExprId>) -> Body {
Body {
exprs: self.exprs,
stmts: self.stmts,
params,
block,
tail,
source_map: BodySourceMap {
expr_ranges: self.expr_ranges,
stmt_ranges: self.stmt_ranges,
},
}
}
fn alloc_expr(&mut self, expr: Expr, range: TextRange) -> ExprId {
let id = ExprId(u32::try_from(self.exprs.len()).unwrap_or(u32::MAX));
self.exprs.push(expr);
self.expr_ranges.push(range);
id
}
fn alloc_stmt(&mut self, stmt: Stmt, range: TextRange) -> StmtId {
let id = StmtId(u32::try_from(self.stmts.len()).unwrap_or(u32::MAX));
self.stmts.push(stmt);
self.stmt_ranges.push(range);
id
}
fn missing(&mut self, range: TextRange) -> ExprId {
self.alloc_expr(Expr::Missing, range)
}
fn lower_first_expr(&mut self, node: &GdNode) -> ExprId {
match cst::first_child_expr(node) {
Some(c) => self.lower_expr(&c),
None => self.missing(cst::text_range_of(node)),
}
}
#[allow(clippy::too_many_lines)]
fn lower_expr(&mut self, node: &GdNode) -> ExprId {
use SyntaxKind as K;
let range = cst::text_range_of(node);
let expr = match node.kind() {
K::Literal => Expr::Literal(literal_kind(node)),
K::NameRef => return self.lower_name_ref(node),
K::ParenExpr => Expr::Paren(self.lower_first_expr(node)),
K::BinExpr => {
let exprs = cst::child_exprs(node);
let op = bin_op(node).unwrap_or(BinOp::Add);
let lhs = self.lower_or_missing(exprs.first(), range);
let rhs = self.lower_or_missing(exprs.get(1), range);
Expr::Bin { op, lhs, rhs }
}
K::UnaryExpr => {
let op = un_op(node).unwrap_or(UnOp::Pos);
let operand = self.lower_first_expr(node);
Expr::Unary { op, operand }
}
K::AwaitExpr => Expr::Await(self.lower_first_expr(node)),
K::TernaryExpr => {
let exprs = cst::child_exprs(node);
let then_branch = self.lower_or_missing(exprs.first(), range);
let cond = self.lower_or_missing(exprs.get(1), range);
let else_branch = self.lower_or_missing(exprs.get(2), range);
Expr::Ternary {
cond,
then_branch,
else_branch,
}
}
K::CallExpr => {
if let Some(path) = get_node_call_path(node) {
Expr::GetNode {
path: Some(path),
unique: false,
}
} else {
let callee = self.lower_first_expr(node);
let args = cst::first_child(node, |k| k == K::ArgList)
.map(|al| self.lower_exprs(&al))
.unwrap_or_default();
Expr::Call { callee, args }
}
}
K::IndexExpr => {
let exprs = cst::child_exprs(node);
let base = self.lower_or_missing(exprs.first(), range);
let index = self.lower_or_missing(exprs.get(1), range);
Expr::Index { base, index }
}
K::FieldExpr => {
let receiver = self.lower_first_expr(node);
let (name, name_range) = field_member(node).unwrap_or((SmolStr::default(), range));
Expr::Field {
receiver,
name,
name_range,
}
}
K::IsExpr => {
let operand = self.lower_first_expr(node);
Expr::Is {
operand,
ty: type_ref_ptr(node),
negated: cst::has_token(node, K::NotKw),
}
}
K::CastExpr => {
let operand = self.lower_first_expr(node);
Expr::Cast {
operand,
ty: type_ref_ptr(node),
}
}
K::InExpr => {
let exprs = cst::child_exprs(node);
let lhs = self.lower_or_missing(exprs.first(), range);
let rhs = self.lower_or_missing(exprs.get(1), range);
Expr::In {
lhs,
rhs,
negated: cst::has_token(node, K::NotKw),
}
}
K::ArrayLit => Expr::Array(self.lower_exprs(node)),
K::DictLit => {
let entries = cst::children_of(node, K::DictEntry)
.iter()
.map(|e| {
let kv = cst::child_exprs(e);
let key = self.lower_or_missing(kv.first(), cst::text_range_of(e));
let value = kv.get(1).map(|v| self.lower_expr(v));
(key, value)
})
.collect();
Expr::Dict(entries)
}
K::LambdaExpr => {
let params = cst::first_child(node, |k| k == K::ParamList)
.map(|pl| self.lower_params(&pl))
.unwrap_or_default();
let body = cst::first_child(node, |k| k == K::Block)
.map(|b| self.lower_block(&b))
.unwrap_or_default();
Expr::Lambda { params, body }
}
K::PreloadExpr => {
let arg_node = cst::first_child(node, |k| k == K::ArgList)
.and_then(|al| cst::first_child_expr(&al));
let path = arg_node
.as_ref()
.filter(|n| n.kind() == K::Literal)
.and_then(|n| cst::child_token_text(n, K::String))
.map(|s| SmolStr::new(s.trim_matches(['"', '\''])));
let arg = arg_node.map(|e| self.lower_expr(&e));
Expr::Preload { arg, path }
}
K::GetNodeExpr | K::UniqueNodeExpr => Expr::GetNode {
path: node_path_text(node),
unique: node.kind() == K::UniqueNodeExpr,
},
_ => Expr::Missing,
};
self.alloc_expr(expr, range)
}
fn lower_name_ref(&mut self, node: &GdNode) -> ExprId {
let range = cst::text_range_of(node);
let expr = match cst::first_token(node) {
Some(t) if t.kind() == SyntaxKind::SelfKw => Expr::SelfExpr,
Some(t) if t.kind() == SyntaxKind::SuperKw => Expr::Super,
Some(t) => Expr::Name(SmolStr::new(t.text())),
None => Expr::Missing,
};
self.alloc_expr(expr, range)
}
fn lower_or_missing(&mut self, node: Option<&GdNode>, fallback: TextRange) -> ExprId {
match node {
Some(n) => self.lower_expr(n),
None => self.missing(fallback),
}
}
fn lower_exprs(&mut self, node: &GdNode) -> Vec<ExprId> {
cst::child_exprs(node)
.iter()
.map(|c| self.lower_expr(c))
.collect()
}
fn lower_params(&mut self, param_list: &GdNode) -> Vec<ParamBinding> {
cst::children_of(param_list, SyntaxKind::Param)
.iter()
.filter_map(|p| {
let name_tok = ast::Param::cast(p.clone())?.name()?;
let name_node = name_tok.syntax();
Some(ParamBinding {
name: SmolStr::new(name_tok.text()?),
type_ref: type_ref_ptr(p),
default: cst::first_child_expr(p).map(|e| self.lower_expr(&e)),
name_range: cst::text_range_of(name_node),
})
})
.collect()
}
fn lower_block(&mut self, block: &GdNode) -> Block {
block
.children()
.filter_map(|c| self.lower_stmt(c))
.collect()
}
fn lower_stmt(&mut self, node: &GdNode) -> Option<StmtId> {
use SyntaxKind as K;
let range = cst::text_range_of(node);
let stmt = match node.kind() {
K::ExprStmt => Stmt::Expr(self.lower_first_expr(node)),
K::VarDecl | K::ConstDecl => Stmt::Var(self.lower_local_var(node)),
K::ReturnStmt => Stmt::Return(cst::first_child_expr(node).map(|e| self.lower_expr(&e))),
K::IfStmt => self.lower_if(node),
K::WhileStmt => Stmt::While {
cond: self.lower_first_expr(node),
body: self.lower_child_block(node),
},
K::ForStmt => Stmt::For(self.lower_for(node)),
K::MatchStmt => self.lower_match(node),
K::BreakStmt => Stmt::Break,
K::ContinueStmt => Stmt::Continue,
K::PassStmt | K::BreakpointStmt => Stmt::Pass,
K::AssertStmt => Stmt::Assert(
cst::first_child(node, |k| k == K::ArgList)
.and_then(|al| cst::first_child_expr(&al))
.map(|e| self.lower_expr(&e)),
),
_ => return None,
};
Some(self.alloc_stmt(stmt, range))
}
fn lower_local_var(&mut self, node: &GdNode) -> LocalVar {
let name_node = cst::first_child(node, |k| k == SyntaxKind::Name);
let name = name_node
.as_ref()
.and_then(|n| ast::Name::cast(n.clone()))
.and_then(|n| n.text())
.map(SmolStr::new)
.unwrap_or_default();
LocalVar {
name,
type_ref: type_ref_ptr(node),
init: cst::first_child_expr(node).map(|e| self.lower_expr(&e)),
is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
is_const: node.kind() == SyntaxKind::ConstDecl,
name_range: name_node
.as_ref()
.map_or_else(|| cst::text_range_of(node), cst::text_range_of),
}
}
fn lower_if(&mut self, node: &GdNode) -> Stmt {
let cond = self.lower_first_expr(node);
let then_branch = self.lower_child_block(node);
let elifs = cst::children_of(node, SyntaxKind::ElifClause)
.iter()
.map(|c| (self.lower_first_expr(c), self.lower_child_block(c)))
.collect();
let else_branch = cst::first_child(node, |k| k == SyntaxKind::ElseClause)
.map(|c| self.lower_child_block(&c));
Stmt::If {
cond,
then_branch,
elifs,
else_branch,
}
}
fn lower_for(&mut self, node: &GdNode) -> ForLoop {
let name = cst::first_child(node, |k| k == SyntaxKind::Name);
let var = name
.as_ref()
.and_then(|n| ast::Name::cast(n.clone()))
.and_then(|n| n.text())
.map(SmolStr::new)
.unwrap_or_default();
ForLoop {
var,
var_type: type_ref_ptr(node),
var_range: name
.as_ref()
.map_or_else(|| cst::text_range_of(node), cst::text_range_of),
iter: self.lower_first_expr(node),
body: self.lower_child_block(node),
}
}
fn lower_match(&mut self, node: &GdNode) -> Stmt {
let scrutinee = self.lower_first_expr(node);
let arms = cst::children_of(node, SyntaxKind::MatchArm)
.iter()
.map(|arm| {
let binds = cst::children_of(arm, SyntaxKind::PatternBind)
.iter()
.filter_map(|b| {
let name_node = cst::first_child(b, |k| k == SyntaxKind::Name)?;
let name = ast::Name::cast(name_node.clone())?
.text()
.map(SmolStr::new)?;
Some(MatchBind {
name,
range: cst::text_range_of(&name_node),
})
})
.collect();
let guard = cst::first_child(arm, |k| k == SyntaxKind::PatternGuard)
.and_then(|g| cst::first_child_expr(&g))
.map(|e| self.lower_expr(&e));
let body = self.lower_child_block(arm);
MatchArm {
binds,
guard,
body,
range: cst::text_range_of(arm),
is_catch_all: arm_is_unconditional_catch_all(arm),
}
})
.collect();
Stmt::Match { scrutinee, arms }
}
fn lower_child_block(&mut self, node: &GdNode) -> Block {
cst::first_child(node, |k| k == SyntaxKind::Block)
.map(|b| self.lower_block(&b))
.unwrap_or_default()
}
}
fn type_ref_ptr(node: &GdNode) -> Option<AstPtr> {
cst::first_child(node, |k| k == SyntaxKind::TypeRef).map(|t| AstPtr::of(&t))
}
fn literal_kind(node: &GdNode) -> Literal {
use SyntaxKind as K;
match cst::first_token(node).map(|t| t.kind()) {
Some(K::Int) => Literal::Int,
Some(K::Float) => Literal::Float,
Some(K::String) => Literal::Str,
Some(K::StringName) => Literal::StringName,
Some(K::NodePath) => Literal::NodePath,
Some(K::True | K::False) => Literal::Bool,
Some(K::ConstPi | K::ConstTau | K::ConstInf | K::ConstNan) => Literal::MathConst,
_ => Literal::Null,
}
}
fn bin_op(node: &GdNode) -> Option<BinOp> {
node.children_with_tokens()
.filter_map(cstree::util::NodeOrToken::into_token)
.find_map(|t| BinOp::from_token(t.kind()))
}
fn un_op(node: &GdNode) -> Option<UnOp> {
node.children_with_tokens()
.filter_map(cstree::util::NodeOrToken::into_token)
.find_map(|t| UnOp::from_token(t.kind()))
}
fn field_member(node: &GdNode) -> Option<(SmolStr, TextRange)> {
let nameref = cst::children_of(node, SyntaxKind::NameRef).pop()?;
let tok = cst::first_token(&nameref)?;
Some((SmolStr::new(tok.text()), cst::token_range(&tok)))
}
fn get_node_call_path(node: &GdNode) -> Option<SmolStr> {
let callee = cst::first_child_expr(node)?;
let is_get_node = match callee.kind() {
SyntaxKind::NameRef => {
cst::first_token(&callee).is_some_and(|t| is_get_node_name(t.text()))
}
SyntaxKind::FieldExpr => {
is_self_receiver(&callee)
&& field_member(&callee).is_some_and(|(name, _)| is_get_node_name(&name))
}
_ => false,
};
if !is_get_node {
return None;
}
let arg = cst::first_child(node, |k| k == SyntaxKind::ArgList)
.and_then(|al| cst::first_child_expr(&al))?;
if arg.kind() != SyntaxKind::Literal {
return None; }
let s = cst::child_token_text(&arg, SyntaxKind::String)?;
Some(SmolStr::new(s.trim_matches(['"', '\''])))
}
fn is_get_node_name(name: &str) -> bool {
matches!(name, "get_node" | "get_node_or_null")
}
fn is_self_receiver(field_expr: &GdNode) -> bool {
cst::first_child_expr(field_expr).is_some_and(|recv| {
recv.kind() == SyntaxKind::NameRef
&& recv
.children_with_tokens()
.filter_map(cstree::util::NodeOrToken::into_token)
.any(|t| t.kind() == SyntaxKind::SelfKw)
})
}
fn node_path_text(node: &GdNode) -> Option<SmolStr> {
if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
return Some(SmolStr::new(s.trim_matches(['"', '\''])));
}
let segs: Vec<String> = node
.children_with_tokens()
.filter_map(cstree::util::NodeOrToken::into_token)
.filter(|t| t.kind() == SyntaxKind::Ident)
.map(|t| t.text().to_owned())
.collect();
(!segs.is_empty()).then(|| SmolStr::new(segs.join("/")))
}
#[cfg(test)]
mod tests {
use super::*;
use gdscript_syntax::parse;
fn func_body(src: &str) -> Body {
let root = parse(src).syntax_node();
let func = gdscript_syntax::ast::descendants(&root)
.into_iter()
.find(|n| n.kind() == SyntaxKind::FuncDecl)
.expect("a FuncDecl");
body_of_func(&func)
}
#[test]
fn lowers_params_and_return() {
let body = func_body("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
assert_eq!(body.params.len(), 2);
assert_eq!(body.params[0].name, "a");
assert!(body.params[0].type_ref.is_some());
assert!(body.params[1].default.is_some());
assert_eq!(body.block.len(), 1);
let Stmt::Return(Some(ret)) = body.stmt(body.block[0]) else {
panic!("expected return")
};
assert!(matches!(body.expr(*ret), Expr::Bin { op: BinOp::Add, .. }));
}
#[test]
fn lowers_local_var_and_field_and_call() {
let body = func_body("func f():\n\tvar n := get_node(\"x\")\n\tn.show()\n");
let Stmt::Var(v) = body.stmt(body.block[0]) else {
panic!("expected var")
};
assert_eq!(v.name, "n");
assert!(v.is_inferred && v.init.is_some());
let Stmt::Expr(e) = body.stmt(body.block[1]) else {
panic!("expected expr stmt")
};
let Expr::Call { callee, .. } = body.expr(*e) else {
panic!("expected call")
};
assert!(matches!(body.expr(*callee), Expr::Field { name, .. } if name == "show"));
}
#[test]
fn lowers_if_with_is_narrowing() {
let body = func_body("func f(x):\n\tif x is Node:\n\t\tx.free()\n\telse:\n\t\tpass\n");
let Stmt::If {
cond,
then_branch,
else_branch,
..
} = body.stmt(body.block[0])
else {
panic!("expected if")
};
assert!(matches!(body.expr(*cond), Expr::Is { negated: false, .. }));
assert_eq!(then_branch.len(), 1);
assert!(else_branch.is_some());
}
#[test]
fn source_map_finds_tightest_expr() {
let body = func_body("func f(a, b):\n\treturn a + b\n");
let b_offset = u32::try_from("func f(a, b):\n\treturn a + ".len()).unwrap();
let id = body
.source_map
.expr_at_offset(b_offset)
.expect("an expr at b");
assert!(matches!(body.expr(id), Expr::Name(n) if n == "b"));
}
#[test]
fn initializer_body_has_tail() {
let root = parse("var x = 1 + 2\n").syntax_node();
let var = gdscript_syntax::ast::descendants(&root)
.into_iter()
.find(|n| n.kind() == SyntaxKind::VarDecl)
.unwrap();
let init = crate::cst::first_child_expr(&var).unwrap();
let body = body_of_expr(&init);
assert!(body.tail.is_some());
assert!(matches!(
body.expr(body.tail.unwrap()),
Expr::Bin { op: BinOp::Add, .. }
));
}
}