Skip to main content

gdscript_hir/
body.rs

1//! Body lowering (Playbook §3.1/§3.4): a function body (or a class-level initializer
2//! expression) lowered from the CST into a flat arena of [`Expr`]/[`Stmt`] addressed by
3//! [`ExprId`]/[`StmtId`], plus a [`BodySourceMap`] mapping every [`ExprId`] back to its byte
4//! range. Every IDE feature (hover, inlay, completion) maps a cursor offset → `ExprId` through
5//! this source map, then reads the inferred type from [`crate::infer`].
6//!
7//! Lowering is a pure function of the CST: no engine API, no name resolution, no types. Type
8//! annotations (`is`/`as`/`var`/param/`for`) are kept as [`AstPtr`]s to their `TypeRef` nodes
9//! and resolved later, so this stage never depends on the model.
10
11use gdscript_base::TextRange;
12use gdscript_syntax::ast::{self, AstNode};
13use gdscript_syntax::{GdNode, SyntaxKind};
14use smol_str::SmolStr;
15
16use crate::cst::{self, AstPtr};
17
18/// An index into [`Body::exprs`].
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct ExprId(pub u32);
21
22/// An index into [`Body::stmts`].
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct StmtId(pub u32);
25
26/// A lowered block: its statements, in order.
27pub type Block = Vec<StmtId>;
28
29/// A literal's kind (the value text lives in the CST; only the kind drives typing).
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Literal {
32    /// An integer literal.
33    Int,
34    /// A float literal.
35    Float,
36    /// `true` / `false`.
37    Bool,
38    /// A `String` literal.
39    Str,
40    /// A `&"…"` `StringName` literal.
41    StringName,
42    /// A `^"…"` `NodePath` literal.
43    NodePath,
44    /// `null`.
45    Null,
46    /// `PI` / `TAU` / `INF` / `NAN` (a `float`).
47    MathConst,
48}
49
50/// A binary operator.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum BinOp {
53    /// `+`
54    Add,
55    /// `-`
56    Sub,
57    /// `*`
58    Mul,
59    /// `/`
60    Div,
61    /// `%`
62    Mod,
63    /// `**`
64    Pow,
65    /// `==`
66    Eq,
67    /// `!=`
68    Ne,
69    /// `<`
70    Lt,
71    /// `>`
72    Gt,
73    /// `<=`
74    Le,
75    /// `>=`
76    Ge,
77    /// `and` / `&&`
78    And,
79    /// `or` / `||`
80    Or,
81    /// `&`
82    BitAnd,
83    /// `|`
84    BitOr,
85    /// `^`
86    BitXor,
87    /// `<<`
88    Shl,
89    /// `>>`
90    Shr,
91    /// `=` (assignment) or any compound assignment (`+=`, `<<=`, …).
92    Assign,
93}
94
95impl BinOp {
96    /// Map an operator token kind to a [`BinOp`]. Compound assignments collapse to
97    /// [`BinOp::Assign`] (the typing rule is the same: check the RHS against the LHS slot).
98    #[must_use]
99    pub fn from_token(kind: SyntaxKind) -> Option<Self> {
100        use SyntaxKind as K;
101        Some(match kind {
102            K::Plus => Self::Add,
103            K::Minus => Self::Sub,
104            K::Star => Self::Mul,
105            K::Slash => Self::Div,
106            K::Percent => Self::Mod,
107            K::StarStar => Self::Pow,
108            K::EqEq => Self::Eq,
109            K::Neq => Self::Ne,
110            K::Lt => Self::Lt,
111            K::Gt => Self::Gt,
112            K::Le => Self::Le,
113            K::Ge => Self::Ge,
114            K::AndKw | K::AmpAmp => Self::And,
115            K::OrKw | K::PipePipe => Self::Or,
116            K::Amp => Self::BitAnd,
117            K::Pipe => Self::BitOr,
118            K::Caret => Self::BitXor,
119            K::Shl => Self::Shl,
120            K::Shr => Self::Shr,
121            K::Eq
122            | K::PlusEq
123            | K::MinusEq
124            | K::StarEq
125            | K::SlashEq
126            | K::PercentEq
127            | K::StarStarEq
128            | K::AmpEq
129            | K::PipeEq
130            | K::CaretEq
131            | K::ShlEq
132            | K::ShrEq => Self::Assign,
133            _ => return None,
134        })
135    }
136
137    /// Whether this is an arithmetic operator (`+ - * / % **`).
138    #[must_use]
139    pub fn is_arithmetic(self) -> bool {
140        matches!(
141            self,
142            Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod | Self::Pow
143        )
144    }
145
146    /// Whether this is a comparison / logical operator (result is `bool`).
147    #[must_use]
148    pub fn is_boolean(self) -> bool {
149        matches!(
150            self,
151            Self::Eq | Self::Ne | Self::Lt | Self::Gt | Self::Le | Self::Ge | Self::And | Self::Or
152        )
153    }
154}
155
156/// A prefix unary operator.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum UnOp {
159    /// `-`
160    Neg,
161    /// `+`
162    Pos,
163    /// `not` / `!`
164    Not,
165    /// `~`
166    BitNot,
167}
168
169impl UnOp {
170    /// Map a prefix operator token kind to a [`UnOp`].
171    #[must_use]
172    pub fn from_token(kind: SyntaxKind) -> Option<Self> {
173        Some(match kind {
174            SyntaxKind::Minus => Self::Neg,
175            SyntaxKind::Plus => Self::Pos,
176            SyntaxKind::NotKw | SyntaxKind::Bang => Self::Not,
177            SyntaxKind::Tilde => Self::BitNot,
178            _ => return None,
179        })
180    }
181}
182
183/// A lowered expression. Children are referenced by [`ExprId`].
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub enum Expr {
186    /// An unlowerable / recovered expression (typed `Error`, suppresses cascade).
187    Missing,
188    /// A literal.
189    Literal(Literal),
190    /// A bare identifier reference.
191    Name(SmolStr),
192    /// `self`.
193    SelfExpr,
194    /// `super`.
195    Super,
196    /// A binary expression.
197    Bin {
198        /// The operator.
199        op: BinOp,
200        /// Left operand.
201        lhs: ExprId,
202        /// Right operand.
203        rhs: ExprId,
204    },
205    /// A prefix unary expression.
206    Unary {
207        /// The operator.
208        op: UnOp,
209        /// The operand.
210        operand: ExprId,
211    },
212    /// `a if c else b`.
213    Ternary {
214        /// The condition.
215        cond: ExprId,
216        /// Value when the condition holds.
217        then_branch: ExprId,
218        /// Value otherwise.
219        else_branch: ExprId,
220    },
221    /// `callee(args…)`.
222    Call {
223        /// The callee.
224        callee: ExprId,
225        /// The argument expressions.
226        args: Vec<ExprId>,
227    },
228    /// `receiver.name`.
229    Field {
230        /// The receiver.
231        receiver: ExprId,
232        /// The member name.
233        name: SmolStr,
234        /// The member-name token range (for hover / member-completion context).
235        name_range: TextRange,
236    },
237    /// `base[index]`.
238    Index {
239        /// The indexed value.
240        base: ExprId,
241        /// The index expression.
242        index: ExprId,
243    },
244    /// `operand is [not] T` — always `bool`; narrows on the true branch.
245    Is {
246        /// The tested operand.
247        operand: ExprId,
248        /// The `TypeRef` node tested against.
249        ty: Option<AstPtr>,
250        /// Whether it was `is not`.
251        negated: bool,
252    },
253    /// `operand as T` — optimistic downcast to `T`.
254    Cast {
255        /// The operand.
256        operand: ExprId,
257        /// The target `TypeRef` node.
258        ty: Option<AstPtr>,
259    },
260    /// `lhs [not] in rhs` — always `bool`.
261    In {
262        /// The needle.
263        lhs: ExprId,
264        /// The haystack.
265        rhs: ExprId,
266        /// Whether it was `not in`.
267        negated: bool,
268    },
269    /// `await operand`.
270    Await(ExprId),
271    /// `[a, b, …]`.
272    Array(Vec<ExprId>),
273    /// `{ k: v, … }` (value is `None` only on recovery).
274    Dict(Vec<(ExprId, Option<ExprId>)>),
275    /// `func(...): …` — an anonymous function (typed `Callable`).
276    Lambda {
277        /// The lambda parameters.
278        params: Vec<ParamBinding>,
279        /// The lambda body.
280        body: Block,
281    },
282    /// `preload(path)` — a compile-time resource reference. When `path` is a constant string
283    /// literal (the only form Godot accepts), it is captured here so inference can resolve it to
284    /// the declaring file's `ScriptRef` (M3); a non-literal argument leaves `path` `None` (the
285    /// seam).
286    Preload {
287        /// The lowered path argument expression, if present (kept so it is still type-walked).
288        arg: Option<ExprId>,
289        /// The constant-folded path string (unquoted), when the argument is a string literal.
290        path: Option<SmolStr>,
291    },
292    /// `$Path` / `%Unique` / `get_node("…")` — a node-path access. In Phase 2 this was always
293    /// `Object(Node)`; Phase-4 M1 resolves the literal path against the owning scene to the node's
294    /// concrete type. A computed `get_node(var)` keeps `path: None` (stays `Node`, never warns).
295    GetNode {
296        /// The literal node path (`"Panel/VBox/Button"`, or a `%Unique` name), or `None` if computed.
297        path: Option<SmolStr>,
298        /// `true` for the `%Unique` form (resolve via `unique_name_in_owner`); `false` for `$Path`.
299        unique: bool,
300    },
301    /// `(inner)`.
302    Paren(ExprId),
303}
304
305/// A function / lambda parameter binding.
306#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct ParamBinding {
308    /// The parameter name.
309    pub name: SmolStr,
310    /// The `TypeRef` annotation node, if written.
311    pub type_ref: Option<AstPtr>,
312    /// The default-value expression, if written.
313    pub default: Option<ExprId>,
314    /// The name token range.
315    pub name_range: TextRange,
316}
317
318/// A local `var` / `const` declaration.
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct LocalVar {
321    /// The binding name.
322    pub name: SmolStr,
323    /// The `TypeRef` annotation node, if written.
324    pub type_ref: Option<AstPtr>,
325    /// The initializer expression, if written.
326    pub init: Option<ExprId>,
327    /// Whether the type was inferred with `:=`.
328    pub is_inferred: bool,
329    /// Whether this is a `const`.
330    pub is_const: bool,
331    /// The name token range.
332    pub name_range: TextRange,
333}
334
335/// A `for var [: T] in iter:` loop.
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct ForLoop {
338    /// The loop variable name.
339    pub var: SmolStr,
340    /// The loop variable's `TypeRef` annotation node (4.2+ `for x: T in …`), if written.
341    pub var_type: Option<AstPtr>,
342    /// The loop variable's name token range.
343    pub var_range: TextRange,
344    /// The iterated expression.
345    pub iter: ExprId,
346    /// The loop body.
347    pub body: Block,
348}
349
350/// One `var x` capture in a `match` pattern — a local binding (so navigation can find/rename it).
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct MatchBind {
353    /// The captured name.
354    pub name: SmolStr,
355    /// The capture's name-token range (may carry leading whitespace, like other body bindings).
356    pub range: TextRange,
357}
358
359/// One `match` arm.
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct MatchArm {
362    /// Names bound by `var x` patterns in this arm (typed `Variant` in Phase 2).
363    pub binds: Vec<MatchBind>,
364    /// The `when` guard expression, if any.
365    pub guard: Option<ExprId>,
366    /// The arm body.
367    pub body: Block,
368    /// The arm's byte range (the `UNREACHABLE_PATTERN` anchor).
369    pub range: TextRange,
370    /// Whether this arm is an **unconditional catch-all** — its sole top-level pattern is `_` or a
371    /// `var x` bind, with no `when` guard. Every arm *after* a catch-all is `UNREACHABLE_PATTERN`.
372    pub is_catch_all: bool,
373}
374
375/// Whether a `match` arm is an UNCONDITIONAL catch-all — its **sole top-level** pattern is `_` (a
376/// `PatternLiteral`/`PatternWildcard` whose only token is `_`) or a `var x` bind (`PatternBind`),
377/// and it has no `when` guard. Conservative: a multi-pattern arm (`1, _:`), a `_`/`var` nested in an
378/// array/dict pattern, or a guarded arm is NOT a catch-all — we under-warn `UNREACHABLE_PATTERN`
379/// rather than risk flagging a reachable arm (a false positive on valid code).
380fn arm_is_unconditional_catch_all(arm: &GdNode) -> bool {
381    use SyntaxKind as K;
382    if cst::first_child(arm, |k| k == K::PatternGuard).is_some() {
383        return false;
384    }
385    let patterns: Vec<&GdNode> = arm
386        .children()
387        .filter(|c| {
388            matches!(
389                c.kind(),
390                K::PatternBind
391                    | K::PatternLiteral
392                    | K::PatternWildcard
393                    | K::PatternArray
394                    | K::PatternDict
395                    | K::PatternRest
396            )
397        })
398        .collect();
399    let [only] = patterns.as_slice() else {
400        return false;
401    };
402    match only.kind() {
403        K::PatternBind | K::PatternWildcard => true,
404        // `_` parses as a `PatternLiteral` wrapping the identifier expr `_` (a `NameRef` node), so
405        // the `_` token is nested one level down — check the inner expr's first token.
406        K::PatternLiteral => cst::first_child_expr(only)
407            .and_then(|e| cst::first_token(&e))
408            .is_some_and(|t| t.text() == "_"),
409        _ => false,
410    }
411}
412
413/// A lowered statement.
414#[derive(Debug, Clone, PartialEq, Eq)]
415pub enum Stmt {
416    /// An expression statement.
417    Expr(ExprId),
418    /// A local `var` / `const`.
419    Var(LocalVar),
420    /// `return [expr]`.
421    Return(Option<ExprId>),
422    /// `if … elif … else …`.
423    If {
424        /// The `if` condition.
425        cond: ExprId,
426        /// The `if` branch.
427        then_branch: Block,
428        /// The `elif` branches.
429        elifs: Vec<(ExprId, Block)>,
430        /// The `else` branch.
431        else_branch: Option<Block>,
432    },
433    /// `while cond:`.
434    While {
435        /// The loop condition.
436        cond: ExprId,
437        /// The loop body.
438        body: Block,
439    },
440    /// `for …:`.
441    For(ForLoop),
442    /// `match …:`.
443    Match {
444        /// The matched value.
445        scrutinee: ExprId,
446        /// The arms.
447        arms: Vec<MatchArm>,
448    },
449    /// `break`.
450    Break,
451    /// `continue`.
452    Continue,
453    /// `pass` / `breakpoint`.
454    Pass,
455    /// `assert(cond[, msg])`.
456    Assert(Option<ExprId>),
457}
458
459/// Maps every [`ExprId`]/[`StmtId`] back to its source byte range. The reverse direction (offset →
460/// `ExprId`) is the tightest containing expression.
461#[derive(Debug, Clone, Default, PartialEq, Eq)]
462pub struct BodySourceMap {
463    expr_ranges: Vec<TextRange>,
464    stmt_ranges: Vec<TextRange>,
465}
466
467impl BodySourceMap {
468    /// The source range of an expression.
469    #[must_use]
470    pub fn expr_range(&self, id: ExprId) -> TextRange {
471        self.expr_ranges[id.0 as usize]
472    }
473
474    /// The source range of a statement (the whole statement node — the `UNREACHABLE_CODE` anchor).
475    #[must_use]
476    pub fn stmt_range(&self, id: StmtId) -> TextRange {
477        self.stmt_ranges[id.0 as usize]
478    }
479
480    /// The innermost (tightest) expression whose range contains `offset`.
481    #[must_use]
482    pub fn expr_at_offset(&self, offset: u32) -> Option<ExprId> {
483        self.expr_ranges
484            .iter()
485            .enumerate()
486            .filter(|(_, r)| r.start <= offset && offset < r.end)
487            .min_by_key(|(_, r)| r.end - r.start)
488            .map(|(i, _)| ExprId(u32::try_from(i).unwrap_or(u32::MAX)))
489    }
490
491    /// The expression whose range exactly equals `range` (for mapping a CST node back to its
492    /// `ExprId` — e.g. a member-completion receiver).
493    #[must_use]
494    pub fn expr_for_range(&self, range: TextRange) -> Option<ExprId> {
495        self.expr_ranges
496            .iter()
497            .position(|r| *r == range)
498            .map(|i| ExprId(u32::try_from(i).unwrap_or(u32::MAX)))
499    }
500}
501
502/// A lowered function body, or a single class-level initializer expression.
503#[derive(Debug, Clone, Default, PartialEq, Eq)]
504pub struct Body {
505    /// The expression arena.
506    pub exprs: Vec<Expr>,
507    /// The statement arena.
508    pub stmts: Vec<Stmt>,
509    /// The function parameters (empty for an initializer body).
510    pub params: Vec<ParamBinding>,
511    /// The top-level statements (empty for an initializer body).
512    pub block: Block,
513    /// A bare initializer expression (class-level `var`/`const`); `None` for a function body.
514    pub tail: Option<ExprId>,
515    /// The expr → range map.
516    pub source_map: BodySourceMap,
517}
518
519impl Body {
520    /// The expression behind an id.
521    #[must_use]
522    pub fn expr(&self, id: ExprId) -> &Expr {
523        &self.exprs[id.0 as usize]
524    }
525
526    /// The statement behind an id.
527    #[must_use]
528    pub fn stmt(&self, id: StmtId) -> &Stmt {
529        &self.stmts[id.0 as usize]
530    }
531}
532
533/// Lower a `FuncDecl` node into a [`Body`].
534#[must_use]
535pub fn body_of_func(func: &GdNode) -> Body {
536    let mut low = Lowerer::default();
537    let decl = ast::FuncDecl::cast(func.clone());
538    let params = decl
539        .as_ref()
540        .and_then(ast::FuncDecl::param_list)
541        .map(|pl| low.lower_params(pl.syntax()))
542        .unwrap_or_default();
543    let block = decl
544        .as_ref()
545        .and_then(ast::FuncDecl::body)
546        .map(|b| low.lower_block(b.syntax()))
547        .unwrap_or_default();
548    low.finish(params, block, None)
549}
550
551/// Lower a single expression node into a [`Body`] (a class-level `var`/`const` initializer).
552#[must_use]
553pub fn body_of_expr(expr: &GdNode) -> Body {
554    let mut low = Lowerer::default();
555    let tail = low.lower_expr(expr);
556    low.finish(Vec::new(), Vec::new(), Some(tail))
557}
558
559/// Lower a class-level `VarDecl`/`ConstDecl` node into a [`Body`] holding one local-var
560/// statement — so [`crate::infer`] runs the full annotation/inference checks (and records the
561/// member's binding type) on a class field the same way it does for a local.
562#[must_use]
563pub fn body_of_decl_stmt(decl: &GdNode) -> Body {
564    let mut low = Lowerer::default();
565    let block = low.lower_stmt(decl).into_iter().collect();
566    low.finish(Vec::new(), block, None)
567}
568
569/// Recover the function node for `ptr` from `root` and lower its body.
570#[must_use]
571pub fn body(root: &GdNode, ptr: AstPtr) -> Option<Body> {
572    let node = ptr.to_node(root)?;
573    Some(body_of_func(&node))
574}
575
576#[derive(Default)]
577struct Lowerer {
578    exprs: Vec<Expr>,
579    stmts: Vec<Stmt>,
580    expr_ranges: Vec<TextRange>,
581    stmt_ranges: Vec<TextRange>,
582}
583
584impl Lowerer {
585    fn finish(self, params: Vec<ParamBinding>, block: Block, tail: Option<ExprId>) -> Body {
586        Body {
587            exprs: self.exprs,
588            stmts: self.stmts,
589            params,
590            block,
591            tail,
592            source_map: BodySourceMap {
593                expr_ranges: self.expr_ranges,
594                stmt_ranges: self.stmt_ranges,
595            },
596        }
597    }
598
599    fn alloc_expr(&mut self, expr: Expr, range: TextRange) -> ExprId {
600        let id = ExprId(u32::try_from(self.exprs.len()).unwrap_or(u32::MAX));
601        self.exprs.push(expr);
602        self.expr_ranges.push(range);
603        id
604    }
605
606    fn alloc_stmt(&mut self, stmt: Stmt, range: TextRange) -> StmtId {
607        let id = StmtId(u32::try_from(self.stmts.len()).unwrap_or(u32::MAX));
608        self.stmts.push(stmt);
609        self.stmt_ranges.push(range);
610        id
611    }
612
613    fn missing(&mut self, range: TextRange) -> ExprId {
614        self.alloc_expr(Expr::Missing, range)
615    }
616
617    /// Lower the first child expression, or a `Missing` placeholder spanning `node`.
618    fn lower_first_expr(&mut self, node: &GdNode) -> ExprId {
619        match cst::first_child_expr(node) {
620            Some(c) => self.lower_expr(&c),
621            None => self.missing(cst::text_range_of(node)),
622        }
623    }
624
625    #[allow(clippy::too_many_lines)]
626    fn lower_expr(&mut self, node: &GdNode) -> ExprId {
627        use SyntaxKind as K;
628        let range = cst::text_range_of(node);
629        let expr = match node.kind() {
630            K::Literal => Expr::Literal(literal_kind(node)),
631            K::NameRef => return self.lower_name_ref(node),
632            K::ParenExpr => Expr::Paren(self.lower_first_expr(node)),
633            K::BinExpr => {
634                let exprs = cst::child_exprs(node);
635                let op = bin_op(node).unwrap_or(BinOp::Add);
636                let lhs = self.lower_or_missing(exprs.first(), range);
637                let rhs = self.lower_or_missing(exprs.get(1), range);
638                Expr::Bin { op, lhs, rhs }
639            }
640            K::UnaryExpr => {
641                let op = un_op(node).unwrap_or(UnOp::Pos);
642                let operand = self.lower_first_expr(node);
643                Expr::Unary { op, operand }
644            }
645            K::AwaitExpr => Expr::Await(self.lower_first_expr(node)),
646            K::TernaryExpr => {
647                let exprs = cst::child_exprs(node);
648                let then_branch = self.lower_or_missing(exprs.first(), range);
649                let cond = self.lower_or_missing(exprs.get(1), range);
650                let else_branch = self.lower_or_missing(exprs.get(2), range);
651                Expr::Ternary {
652                    cond,
653                    then_branch,
654                    else_branch,
655                }
656            }
657            K::CallExpr => {
658                // `get_node("literal")` / `get_node_or_null("literal")` types like `$literal`.
659                if let Some(path) = get_node_call_path(node) {
660                    Expr::GetNode {
661                        path: Some(path),
662                        unique: false,
663                    }
664                } else {
665                    let callee = self.lower_first_expr(node);
666                    let args = cst::first_child(node, |k| k == K::ArgList)
667                        .map(|al| self.lower_exprs(&al))
668                        .unwrap_or_default();
669                    Expr::Call { callee, args }
670                }
671            }
672            K::IndexExpr => {
673                let exprs = cst::child_exprs(node);
674                let base = self.lower_or_missing(exprs.first(), range);
675                let index = self.lower_or_missing(exprs.get(1), range);
676                Expr::Index { base, index }
677            }
678            K::FieldExpr => {
679                let receiver = self.lower_first_expr(node);
680                let (name, name_range) = field_member(node).unwrap_or((SmolStr::default(), range));
681                Expr::Field {
682                    receiver,
683                    name,
684                    name_range,
685                }
686            }
687            K::IsExpr => {
688                let operand = self.lower_first_expr(node);
689                Expr::Is {
690                    operand,
691                    ty: type_ref_ptr(node),
692                    negated: cst::has_token(node, K::NotKw),
693                }
694            }
695            K::CastExpr => {
696                let operand = self.lower_first_expr(node);
697                Expr::Cast {
698                    operand,
699                    ty: type_ref_ptr(node),
700                }
701            }
702            K::InExpr => {
703                let exprs = cst::child_exprs(node);
704                let lhs = self.lower_or_missing(exprs.first(), range);
705                let rhs = self.lower_or_missing(exprs.get(1), range);
706                Expr::In {
707                    lhs,
708                    rhs,
709                    negated: cst::has_token(node, K::NotKw),
710                }
711            }
712            K::ArrayLit => Expr::Array(self.lower_exprs(node)),
713            K::DictLit => {
714                let entries = cst::children_of(node, K::DictEntry)
715                    .iter()
716                    .map(|e| {
717                        let kv = cst::child_exprs(e);
718                        let key = self.lower_or_missing(kv.first(), cst::text_range_of(e));
719                        let value = kv.get(1).map(|v| self.lower_expr(v));
720                        (key, value)
721                    })
722                    .collect();
723                Expr::Dict(entries)
724            }
725            K::LambdaExpr => {
726                let params = cst::first_child(node, |k| k == K::ParamList)
727                    .map(|pl| self.lower_params(&pl))
728                    .unwrap_or_default();
729                let body = cst::first_child(node, |k| k == K::Block)
730                    .map(|b| self.lower_block(&b))
731                    .unwrap_or_default();
732                Expr::Lambda { params, body }
733            }
734            K::PreloadExpr => {
735                let arg_node = cst::first_child(node, |k| k == K::ArgList)
736                    .and_then(|al| cst::first_child_expr(&al));
737                // Constant-fold a string-literal path (`preload("res://x.gd")`) so inference can
738                // resolve it. Trim matching quotes, as the `extends "…"` path lowering does.
739                let path = arg_node
740                    .as_ref()
741                    .filter(|n| n.kind() == K::Literal)
742                    .and_then(|n| cst::child_token_text(n, K::String))
743                    .map(|s| SmolStr::new(s.trim_matches(['"', '\''])));
744                let arg = arg_node.map(|e| self.lower_expr(&e));
745                Expr::Preload { arg, path }
746            }
747            K::GetNodeExpr | K::UniqueNodeExpr => Expr::GetNode {
748                path: node_path_text(node),
749                unique: node.kind() == K::UniqueNodeExpr,
750            },
751            _ => Expr::Missing,
752        };
753        self.alloc_expr(expr, range)
754    }
755
756    fn lower_name_ref(&mut self, node: &GdNode) -> ExprId {
757        let range = cst::text_range_of(node);
758        let expr = match cst::first_token(node) {
759            Some(t) if t.kind() == SyntaxKind::SelfKw => Expr::SelfExpr,
760            Some(t) if t.kind() == SyntaxKind::SuperKw => Expr::Super,
761            Some(t) => Expr::Name(SmolStr::new(t.text())),
762            None => Expr::Missing,
763        };
764        self.alloc_expr(expr, range)
765    }
766
767    fn lower_or_missing(&mut self, node: Option<&GdNode>, fallback: TextRange) -> ExprId {
768        match node {
769            Some(n) => self.lower_expr(n),
770            None => self.missing(fallback),
771        }
772    }
773
774    fn lower_exprs(&mut self, node: &GdNode) -> Vec<ExprId> {
775        cst::child_exprs(node)
776            .iter()
777            .map(|c| self.lower_expr(c))
778            .collect()
779    }
780
781    fn lower_params(&mut self, param_list: &GdNode) -> Vec<ParamBinding> {
782        cst::children_of(param_list, SyntaxKind::Param)
783            .iter()
784            .filter_map(|p| {
785                let name_tok = ast::Param::cast(p.clone())?.name()?;
786                let name_node = name_tok.syntax();
787                Some(ParamBinding {
788                    name: SmolStr::new(name_tok.text()?),
789                    type_ref: type_ref_ptr(p),
790                    default: cst::first_child_expr(p).map(|e| self.lower_expr(&e)),
791                    name_range: cst::text_range_of(name_node),
792                })
793            })
794            .collect()
795    }
796
797    fn lower_block(&mut self, block: &GdNode) -> Block {
798        block
799            .children()
800            .filter_map(|c| self.lower_stmt(c))
801            .collect()
802    }
803
804    fn lower_stmt(&mut self, node: &GdNode) -> Option<StmtId> {
805        use SyntaxKind as K;
806        let range = cst::text_range_of(node);
807        let stmt = match node.kind() {
808            K::ExprStmt => Stmt::Expr(self.lower_first_expr(node)),
809            K::VarDecl | K::ConstDecl => Stmt::Var(self.lower_local_var(node)),
810            K::ReturnStmt => Stmt::Return(cst::first_child_expr(node).map(|e| self.lower_expr(&e))),
811            K::IfStmt => self.lower_if(node),
812            K::WhileStmt => Stmt::While {
813                cond: self.lower_first_expr(node),
814                body: self.lower_child_block(node),
815            },
816            K::ForStmt => Stmt::For(self.lower_for(node)),
817            K::MatchStmt => self.lower_match(node),
818            K::BreakStmt => Stmt::Break,
819            K::ContinueStmt => Stmt::Continue,
820            K::PassStmt | K::BreakpointStmt => Stmt::Pass,
821            K::AssertStmt => Stmt::Assert(
822                cst::first_child(node, |k| k == K::ArgList)
823                    .and_then(|al| cst::first_child_expr(&al))
824                    .map(|e| self.lower_expr(&e)),
825            ),
826            // A nested local `func` is a declaration, not a statement we type in Phase 2.
827            _ => return None,
828        };
829        Some(self.alloc_stmt(stmt, range))
830    }
831
832    fn lower_local_var(&mut self, node: &GdNode) -> LocalVar {
833        let name_node = cst::first_child(node, |k| k == SyntaxKind::Name);
834        let name = name_node
835            .as_ref()
836            .and_then(|n| ast::Name::cast(n.clone()))
837            .and_then(|n| n.text())
838            .map(SmolStr::new)
839            .unwrap_or_default();
840        LocalVar {
841            name,
842            type_ref: type_ref_ptr(node),
843            init: cst::first_child_expr(node).map(|e| self.lower_expr(&e)),
844            is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
845            is_const: node.kind() == SyntaxKind::ConstDecl,
846            name_range: name_node
847                .as_ref()
848                .map_or_else(|| cst::text_range_of(node), cst::text_range_of),
849        }
850    }
851
852    fn lower_if(&mut self, node: &GdNode) -> Stmt {
853        let cond = self.lower_first_expr(node);
854        let then_branch = self.lower_child_block(node);
855        let elifs = cst::children_of(node, SyntaxKind::ElifClause)
856            .iter()
857            .map(|c| (self.lower_first_expr(c), self.lower_child_block(c)))
858            .collect();
859        let else_branch = cst::first_child(node, |k| k == SyntaxKind::ElseClause)
860            .map(|c| self.lower_child_block(&c));
861        Stmt::If {
862            cond,
863            then_branch,
864            elifs,
865            else_branch,
866        }
867    }
868
869    fn lower_for(&mut self, node: &GdNode) -> ForLoop {
870        let name = cst::first_child(node, |k| k == SyntaxKind::Name);
871        let var = name
872            .as_ref()
873            .and_then(|n| ast::Name::cast(n.clone()))
874            .and_then(|n| n.text())
875            .map(SmolStr::new)
876            .unwrap_or_default();
877        ForLoop {
878            var,
879            var_type: type_ref_ptr(node),
880            var_range: name
881                .as_ref()
882                .map_or_else(|| cst::text_range_of(node), cst::text_range_of),
883            iter: self.lower_first_expr(node),
884            body: self.lower_child_block(node),
885        }
886    }
887
888    fn lower_match(&mut self, node: &GdNode) -> Stmt {
889        let scrutinee = self.lower_first_expr(node);
890        let arms = cst::children_of(node, SyntaxKind::MatchArm)
891            .iter()
892            .map(|arm| {
893                let binds = cst::children_of(arm, SyntaxKind::PatternBind)
894                    .iter()
895                    .filter_map(|b| {
896                        let name_node = cst::first_child(b, |k| k == SyntaxKind::Name)?;
897                        let name = ast::Name::cast(name_node.clone())?
898                            .text()
899                            .map(SmolStr::new)?;
900                        Some(MatchBind {
901                            name,
902                            range: cst::text_range_of(&name_node),
903                        })
904                    })
905                    .collect();
906                let guard = cst::first_child(arm, |k| k == SyntaxKind::PatternGuard)
907                    .and_then(|g| cst::first_child_expr(&g))
908                    .map(|e| self.lower_expr(&e));
909                let body = self.lower_child_block(arm);
910                MatchArm {
911                    binds,
912                    guard,
913                    body,
914                    range: cst::text_range_of(arm),
915                    is_catch_all: arm_is_unconditional_catch_all(arm),
916                }
917            })
918            .collect();
919        Stmt::Match { scrutinee, arms }
920    }
921
922    /// The (first) `Block` child of `node`, lowered.
923    fn lower_child_block(&mut self, node: &GdNode) -> Block {
924        cst::first_child(node, |k| k == SyntaxKind::Block)
925            .map(|b| self.lower_block(&b))
926            .unwrap_or_default()
927    }
928}
929
930/// The `AstPtr` of a node's (first) direct `TypeRef` child.
931fn type_ref_ptr(node: &GdNode) -> Option<AstPtr> {
932    cst::first_child(node, |k| k == SyntaxKind::TypeRef).map(|t| AstPtr::of(&t))
933}
934
935/// Classify a `Literal` node by its token.
936fn literal_kind(node: &GdNode) -> Literal {
937    use SyntaxKind as K;
938    match cst::first_token(node).map(|t| t.kind()) {
939        Some(K::Int) => Literal::Int,
940        Some(K::Float) => Literal::Float,
941        Some(K::String) => Literal::Str,
942        Some(K::StringName) => Literal::StringName,
943        Some(K::NodePath) => Literal::NodePath,
944        Some(K::True | K::False) => Literal::Bool,
945        Some(K::ConstPi | K::ConstTau | K::ConstInf | K::ConstNan) => Literal::MathConst,
946        _ => Literal::Null,
947    }
948}
949
950/// The binary operator token of a `BinExpr`.
951fn bin_op(node: &GdNode) -> Option<BinOp> {
952    node.children_with_tokens()
953        .filter_map(cstree::util::NodeOrToken::into_token)
954        .find_map(|t| BinOp::from_token(t.kind()))
955}
956
957/// The prefix operator token of a `UnaryExpr`.
958fn un_op(node: &GdNode) -> Option<UnOp> {
959    node.children_with_tokens()
960        .filter_map(cstree::util::NodeOrToken::into_token)
961        .find_map(|t| UnOp::from_token(t.kind()))
962}
963
964/// The member name + its range from a `FieldExpr` (the `NameRef` after the `.`).
965fn field_member(node: &GdNode) -> Option<(SmolStr, TextRange)> {
966    let nameref = cst::children_of(node, SyntaxKind::NameRef).pop()?;
967    let tok = cst::first_token(&nameref)?;
968    Some((SmolStr::new(tok.text()), cst::token_range(&tok)))
969}
970
971/// The literal path of a `get_node("…")` / `get_node_or_null("…")` call (a **bare** call = implicit
972/// `self.get_node`), or `None` if it isn't such a call or the argument is computed (the latter stays
973/// a normal call → `Node`). Lets the call lower to a [`Expr::GetNode`] so it types like `$path`.
974fn get_node_call_path(node: &GdNode) -> Option<SmolStr> {
975    let callee = cst::first_child_expr(node)?;
976    // The callee must be `get_node`/`get_node_or_null`, either **bare** (implicit `self`) or
977    // **`self.<m>`** (explicit self = the same attach node). A *foreign* receiver
978    // (`obj.get_node(...)`) is left as a normal call — its path is relative to another node we can't
979    // resolve here.
980    let is_get_node = match callee.kind() {
981        SyntaxKind::NameRef => {
982            cst::first_token(&callee).is_some_and(|t| is_get_node_name(t.text()))
983        }
984        SyntaxKind::FieldExpr => {
985            is_self_receiver(&callee)
986                && field_member(&callee).is_some_and(|(name, _)| is_get_node_name(&name))
987        }
988        _ => false,
989    };
990    if !is_get_node {
991        return None;
992    }
993    let arg = cst::first_child(node, |k| k == SyntaxKind::ArgList)
994        .and_then(|al| cst::first_child_expr(&al))?;
995    if arg.kind() != SyntaxKind::Literal {
996        return None; // computed `get_node(var)` — stays a normal call (→ Node)
997    }
998    let s = cst::child_token_text(&arg, SyntaxKind::String)?;
999    Some(SmolStr::new(s.trim_matches(['"', '\''])))
1000}
1001
1002fn is_get_node_name(name: &str) -> bool {
1003    matches!(name, "get_node" | "get_node_or_null")
1004}
1005
1006/// Whether a `FieldExpr`'s receiver is `self` (a `NameRef` carrying a `SelfKw` token).
1007fn is_self_receiver(field_expr: &GdNode) -> bool {
1008    cst::first_child_expr(field_expr).is_some_and(|recv| {
1009        recv.kind() == SyntaxKind::NameRef
1010            && recv
1011                .children_with_tokens()
1012                .filter_map(cstree::util::NodeOrToken::into_token)
1013                .any(|t| t.kind() == SyntaxKind::SelfKw)
1014    })
1015}
1016
1017/// The literal node path from a `$Path`/`%Unique` (`GetNodeExpr`/`UniqueNodeExpr`) node: a dequoted
1018/// `$"a/b"` string, or the `/`-joined `Ident` segments of `$a/b`. `None` if it carries no path.
1019fn node_path_text(node: &GdNode) -> Option<SmolStr> {
1020    if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
1021        return Some(SmolStr::new(s.trim_matches(['"', '\''])));
1022    }
1023    let segs: Vec<String> = node
1024        .children_with_tokens()
1025        .filter_map(cstree::util::NodeOrToken::into_token)
1026        .filter(|t| t.kind() == SyntaxKind::Ident)
1027        .map(|t| t.text().to_owned())
1028        .collect();
1029    (!segs.is_empty()).then(|| SmolStr::new(segs.join("/")))
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035    use gdscript_syntax::parse;
1036
1037    fn func_body(src: &str) -> Body {
1038        let root = parse(src).syntax_node();
1039        let func = gdscript_syntax::ast::descendants(&root)
1040            .into_iter()
1041            .find(|n| n.kind() == SyntaxKind::FuncDecl)
1042            .expect("a FuncDecl");
1043        body_of_func(&func)
1044    }
1045
1046    #[test]
1047    fn lowers_params_and_return() {
1048        let body = func_body("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
1049        assert_eq!(body.params.len(), 2);
1050        assert_eq!(body.params[0].name, "a");
1051        assert!(body.params[0].type_ref.is_some());
1052        assert!(body.params[1].default.is_some());
1053        assert_eq!(body.block.len(), 1);
1054        let Stmt::Return(Some(ret)) = body.stmt(body.block[0]) else {
1055            panic!("expected return")
1056        };
1057        assert!(matches!(body.expr(*ret), Expr::Bin { op: BinOp::Add, .. }));
1058    }
1059
1060    #[test]
1061    fn lowers_local_var_and_field_and_call() {
1062        let body = func_body("func f():\n\tvar n := get_node(\"x\")\n\tn.show()\n");
1063        // local var
1064        let Stmt::Var(v) = body.stmt(body.block[0]) else {
1065            panic!("expected var")
1066        };
1067        assert_eq!(v.name, "n");
1068        assert!(v.is_inferred && v.init.is_some());
1069        // n.show() — a call on a field
1070        let Stmt::Expr(e) = body.stmt(body.block[1]) else {
1071            panic!("expected expr stmt")
1072        };
1073        let Expr::Call { callee, .. } = body.expr(*e) else {
1074            panic!("expected call")
1075        };
1076        assert!(matches!(body.expr(*callee), Expr::Field { name, .. } if name == "show"));
1077    }
1078
1079    #[test]
1080    fn lowers_if_with_is_narrowing() {
1081        let body = func_body("func f(x):\n\tif x is Node:\n\t\tx.free()\n\telse:\n\t\tpass\n");
1082        let Stmt::If {
1083            cond,
1084            then_branch,
1085            else_branch,
1086            ..
1087        } = body.stmt(body.block[0])
1088        else {
1089            panic!("expected if")
1090        };
1091        assert!(matches!(body.expr(*cond), Expr::Is { negated: false, .. }));
1092        assert_eq!(then_branch.len(), 1);
1093        assert!(else_branch.is_some());
1094    }
1095
1096    #[test]
1097    fn source_map_finds_tightest_expr() {
1098        // `a + b` — offset on `b` should resolve to the Name(b) expr, not the whole BinExpr.
1099        let body = func_body("func f(a, b):\n\treturn a + b\n");
1100        let b_offset = u32::try_from("func f(a, b):\n\treturn a + ".len()).unwrap();
1101        let id = body
1102            .source_map
1103            .expr_at_offset(b_offset)
1104            .expect("an expr at b");
1105        assert!(matches!(body.expr(id), Expr::Name(n) if n == "b"));
1106    }
1107
1108    #[test]
1109    fn initializer_body_has_tail() {
1110        let root = parse("var x = 1 + 2\n").syntax_node();
1111        let var = gdscript_syntax::ast::descendants(&root)
1112            .into_iter()
1113            .find(|n| n.kind() == SyntaxKind::VarDecl)
1114            .unwrap();
1115        let init = crate::cst::first_child_expr(&var).unwrap();
1116        let body = body_of_expr(&init);
1117        assert!(body.tail.is_some());
1118        assert!(matches!(
1119            body.expr(body.tail.unwrap()),
1120            Expr::Bin { op: BinOp::Add, .. }
1121        ));
1122    }
1123}