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` (carries the value, for constant checks like `ASSERT_ALWAYS_*`).
37    Bool(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                // A compound assignment `x op= y` desugars to `x = (x op y)`. Typing then checks the
637                // REAL result against the slot (`velocity *= 0.5` is `velocity = velocity * 0.5` :
638                // Vector2 — not the scalar `0.5`, which would false-`TYPE_MISMATCH`), and the LHS is
639                // a READ of `x` (so `x += 1` is not falsely `UNUSED`). Lowering the LHS twice is safe
640                // for analysis (the Body IR is never executed, so re-evaluation has no side effect).
641                if op == BinOp::Assign
642                    && let Some(under) = compound_assign_op(node)
643                {
644                    let lhs = self.lower_or_missing(exprs.first(), range);
645                    let lhs_read = self.lower_or_missing(exprs.first(), range);
646                    let rhs = self.lower_or_missing(exprs.get(1), range);
647                    let value = self.alloc_expr(
648                        Expr::Bin {
649                            op: under,
650                            lhs: lhs_read,
651                            rhs,
652                        },
653                        range,
654                    );
655                    Expr::Bin {
656                        op: BinOp::Assign,
657                        lhs,
658                        rhs: value,
659                    }
660                } else {
661                    let lhs = self.lower_or_missing(exprs.first(), range);
662                    let rhs = self.lower_or_missing(exprs.get(1), range);
663                    Expr::Bin { op, lhs, rhs }
664                }
665            }
666            K::UnaryExpr => {
667                let op = un_op(node).unwrap_or(UnOp::Pos);
668                let operand = self.lower_first_expr(node);
669                Expr::Unary { op, operand }
670            }
671            K::AwaitExpr => Expr::Await(self.lower_first_expr(node)),
672            K::TernaryExpr => {
673                let exprs = cst::child_exprs(node);
674                let then_branch = self.lower_or_missing(exprs.first(), range);
675                let cond = self.lower_or_missing(exprs.get(1), range);
676                let else_branch = self.lower_or_missing(exprs.get(2), range);
677                Expr::Ternary {
678                    cond,
679                    then_branch,
680                    else_branch,
681                }
682            }
683            K::CallExpr => {
684                // `get_node("literal")` / `get_node_or_null("literal")` types like `$literal`.
685                if let Some(path) = get_node_call_path(node) {
686                    Expr::GetNode {
687                        path: Some(path),
688                        unique: false,
689                    }
690                } else {
691                    let callee = self.lower_first_expr(node);
692                    let args = cst::first_child(node, |k| k == K::ArgList)
693                        .map(|al| self.lower_exprs(&al))
694                        .unwrap_or_default();
695                    Expr::Call { callee, args }
696                }
697            }
698            K::IndexExpr => {
699                let exprs = cst::child_exprs(node);
700                let base = self.lower_or_missing(exprs.first(), range);
701                let index = self.lower_or_missing(exprs.get(1), range);
702                Expr::Index { base, index }
703            }
704            K::FieldExpr => {
705                let receiver = self.lower_first_expr(node);
706                let (name, name_range) = field_member(node).unwrap_or((SmolStr::default(), range));
707                Expr::Field {
708                    receiver,
709                    name,
710                    name_range,
711                }
712            }
713            K::IsExpr => {
714                let operand = self.lower_first_expr(node);
715                Expr::Is {
716                    operand,
717                    ty: type_ref_ptr(node),
718                    negated: cst::has_token(node, K::NotKw),
719                }
720            }
721            K::CastExpr => {
722                let operand = self.lower_first_expr(node);
723                Expr::Cast {
724                    operand,
725                    ty: type_ref_ptr(node),
726                }
727            }
728            K::InExpr => {
729                let exprs = cst::child_exprs(node);
730                let lhs = self.lower_or_missing(exprs.first(), range);
731                let rhs = self.lower_or_missing(exprs.get(1), range);
732                Expr::In {
733                    lhs,
734                    rhs,
735                    negated: cst::has_token(node, K::NotKw),
736                }
737            }
738            K::ArrayLit => Expr::Array(self.lower_exprs(node)),
739            K::DictLit => {
740                let entries = cst::children_of(node, K::DictEntry)
741                    .iter()
742                    .map(|e| {
743                        let kv = cst::child_exprs(e);
744                        let key = self.lower_or_missing(kv.first(), cst::text_range_of(e));
745                        let value = kv.get(1).map(|v| self.lower_expr(v));
746                        (key, value)
747                    })
748                    .collect();
749                Expr::Dict(entries)
750            }
751            K::LambdaExpr => {
752                let params = cst::first_child(node, |k| k == K::ParamList)
753                    .map(|pl| self.lower_params(&pl))
754                    .unwrap_or_default();
755                let body = cst::first_child(node, |k| k == K::Block)
756                    .map(|b| self.lower_block(&b))
757                    .unwrap_or_default();
758                Expr::Lambda { params, body }
759            }
760            K::PreloadExpr => {
761                let arg_node = cst::first_child(node, |k| k == K::ArgList)
762                    .and_then(|al| cst::first_child_expr(&al));
763                // Constant-fold a string-literal path (`preload("res://x.gd")`) so inference can
764                // resolve it. Trim matching quotes, as the `extends "…"` path lowering does.
765                let path = arg_node
766                    .as_ref()
767                    .filter(|n| n.kind() == K::Literal)
768                    .and_then(|n| cst::child_token_text(n, K::String))
769                    .map(|s| SmolStr::new(s.trim_matches(['"', '\''])));
770                let arg = arg_node.map(|e| self.lower_expr(&e));
771                Expr::Preload { arg, path }
772            }
773            K::GetNodeExpr | K::UniqueNodeExpr => Expr::GetNode {
774                path: node_path_text(node),
775                unique: node.kind() == K::UniqueNodeExpr,
776            },
777            _ => Expr::Missing,
778        };
779        self.alloc_expr(expr, range)
780    }
781
782    fn lower_name_ref(&mut self, node: &GdNode) -> ExprId {
783        let range = cst::text_range_of(node);
784        let expr = match cst::first_token(node) {
785            Some(t) if t.kind() == SyntaxKind::SelfKw => Expr::SelfExpr,
786            Some(t) if t.kind() == SyntaxKind::SuperKw => Expr::Super,
787            Some(t) => Expr::Name(SmolStr::new(t.text())),
788            None => Expr::Missing,
789        };
790        self.alloc_expr(expr, range)
791    }
792
793    fn lower_or_missing(&mut self, node: Option<&GdNode>, fallback: TextRange) -> ExprId {
794        match node {
795            Some(n) => self.lower_expr(n),
796            None => self.missing(fallback),
797        }
798    }
799
800    fn lower_exprs(&mut self, node: &GdNode) -> Vec<ExprId> {
801        cst::child_exprs(node)
802            .iter()
803            .map(|c| self.lower_expr(c))
804            .collect()
805    }
806
807    fn lower_params(&mut self, param_list: &GdNode) -> Vec<ParamBinding> {
808        cst::children_of(param_list, SyntaxKind::Param)
809            .iter()
810            .filter_map(|p| {
811                let name_tok = ast::Param::cast(p.clone())?.name()?;
812                let name_node = name_tok.syntax();
813                Some(ParamBinding {
814                    name: SmolStr::new(name_tok.text()?),
815                    type_ref: type_ref_ptr(p),
816                    default: cst::first_child_expr(p).map(|e| self.lower_expr(&e)),
817                    name_range: cst::text_range_of(name_node),
818                })
819            })
820            .collect()
821    }
822
823    fn lower_block(&mut self, block: &GdNode) -> Block {
824        block
825            .children()
826            .filter_map(|c| self.lower_stmt(c))
827            .collect()
828    }
829
830    fn lower_stmt(&mut self, node: &GdNode) -> Option<StmtId> {
831        use SyntaxKind as K;
832        let range = cst::text_range_of(node);
833        let stmt = match node.kind() {
834            K::ExprStmt => Stmt::Expr(self.lower_first_expr(node)),
835            K::VarDecl | K::ConstDecl => Stmt::Var(self.lower_local_var(node)),
836            K::ReturnStmt => Stmt::Return(cst::first_child_expr(node).map(|e| self.lower_expr(&e))),
837            K::IfStmt => self.lower_if(node),
838            K::WhileStmt => Stmt::While {
839                cond: self.lower_first_expr(node),
840                body: self.lower_child_block(node),
841            },
842            K::ForStmt => Stmt::For(self.lower_for(node)),
843            K::MatchStmt => self.lower_match(node),
844            K::BreakStmt => Stmt::Break,
845            K::ContinueStmt => Stmt::Continue,
846            K::PassStmt | K::BreakpointStmt => Stmt::Pass,
847            K::AssertStmt => Stmt::Assert(
848                cst::first_child(node, |k| k == K::ArgList)
849                    .and_then(|al| cst::first_child_expr(&al))
850                    .map(|e| self.lower_expr(&e)),
851            ),
852            // A nested local `func` is a declaration, not a statement we type in Phase 2.
853            _ => return None,
854        };
855        Some(self.alloc_stmt(stmt, range))
856    }
857
858    fn lower_local_var(&mut self, node: &GdNode) -> LocalVar {
859        let name_node = cst::first_child(node, |k| k == SyntaxKind::Name);
860        let name = name_node
861            .as_ref()
862            .and_then(|n| ast::Name::cast(n.clone()))
863            .and_then(|n| n.text())
864            .map(SmolStr::new)
865            .unwrap_or_default();
866        LocalVar {
867            name,
868            type_ref: type_ref_ptr(node),
869            init: cst::first_child_expr(node).map(|e| self.lower_expr(&e)),
870            is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
871            is_const: node.kind() == SyntaxKind::ConstDecl,
872            name_range: name_node
873                .as_ref()
874                .map_or_else(|| cst::text_range_of(node), cst::text_range_of),
875        }
876    }
877
878    fn lower_if(&mut self, node: &GdNode) -> Stmt {
879        let cond = self.lower_first_expr(node);
880        let then_branch = self.lower_child_block(node);
881        let elifs = cst::children_of(node, SyntaxKind::ElifClause)
882            .iter()
883            .map(|c| (self.lower_first_expr(c), self.lower_child_block(c)))
884            .collect();
885        let else_branch = cst::first_child(node, |k| k == SyntaxKind::ElseClause)
886            .map(|c| self.lower_child_block(&c));
887        Stmt::If {
888            cond,
889            then_branch,
890            elifs,
891            else_branch,
892        }
893    }
894
895    fn lower_for(&mut self, node: &GdNode) -> ForLoop {
896        let name = cst::first_child(node, |k| k == SyntaxKind::Name);
897        let var = name
898            .as_ref()
899            .and_then(|n| ast::Name::cast(n.clone()))
900            .and_then(|n| n.text())
901            .map(SmolStr::new)
902            .unwrap_or_default();
903        ForLoop {
904            var,
905            var_type: type_ref_ptr(node),
906            var_range: name
907                .as_ref()
908                .map_or_else(|| cst::text_range_of(node), cst::text_range_of),
909            iter: self.lower_first_expr(node),
910            body: self.lower_child_block(node),
911        }
912    }
913
914    fn lower_match(&mut self, node: &GdNode) -> Stmt {
915        let scrutinee = self.lower_first_expr(node);
916        let arms = cst::children_of(node, SyntaxKind::MatchArm)
917            .iter()
918            .map(|arm| {
919                let binds = cst::children_of(arm, SyntaxKind::PatternBind)
920                    .iter()
921                    .filter_map(|b| {
922                        let name_node = cst::first_child(b, |k| k == SyntaxKind::Name)?;
923                        let name = ast::Name::cast(name_node.clone())?
924                            .text()
925                            .map(SmolStr::new)?;
926                        Some(MatchBind {
927                            name,
928                            range: cst::text_range_of(&name_node),
929                        })
930                    })
931                    .collect();
932                let guard = cst::first_child(arm, |k| k == SyntaxKind::PatternGuard)
933                    .and_then(|g| cst::first_child_expr(&g))
934                    .map(|e| self.lower_expr(&e));
935                let body = self.lower_child_block(arm);
936                MatchArm {
937                    binds,
938                    guard,
939                    body,
940                    range: cst::text_range_of(arm),
941                    is_catch_all: arm_is_unconditional_catch_all(arm),
942                }
943            })
944            .collect();
945        Stmt::Match { scrutinee, arms }
946    }
947
948    /// The (first) `Block` child of `node`, lowered.
949    fn lower_child_block(&mut self, node: &GdNode) -> Block {
950        cst::first_child(node, |k| k == SyntaxKind::Block)
951            .map(|b| self.lower_block(&b))
952            .unwrap_or_default()
953    }
954}
955
956/// The `AstPtr` of a node's (first) direct `TypeRef` child.
957fn type_ref_ptr(node: &GdNode) -> Option<AstPtr> {
958    cst::first_child(node, |k| k == SyntaxKind::TypeRef).map(|t| AstPtr::of(&t))
959}
960
961/// Classify a `Literal` node by its token.
962fn literal_kind(node: &GdNode) -> Literal {
963    use SyntaxKind as K;
964    match cst::first_token(node).map(|t| t.kind()) {
965        Some(K::Int) => Literal::Int,
966        Some(K::Float) => Literal::Float,
967        Some(K::String) => Literal::Str,
968        Some(K::StringName) => Literal::StringName,
969        Some(K::NodePath) => Literal::NodePath,
970        Some(K::True) => Literal::Bool(true),
971        Some(K::False) => Literal::Bool(false),
972        Some(K::ConstPi | K::ConstTau | K::ConstInf | K::ConstNan) => Literal::MathConst,
973        _ => Literal::Null,
974    }
975}
976
977/// The binary operator token of a `BinExpr`.
978fn bin_op(node: &GdNode) -> Option<BinOp> {
979    node.children_with_tokens()
980        .filter_map(cstree::util::NodeOrToken::into_token)
981        .find_map(|t| BinOp::from_token(t.kind()))
982}
983
984/// The *underlying* operator of a **compound** assignment `BinExpr` (`*=` → `Mul`, `+=` → `Add`, …),
985/// or `None` for a plain `=` / a non-assignment. Used to desugar `x op= y` into `x = (x op y)`.
986fn compound_assign_op(node: &GdNode) -> Option<BinOp> {
987    use SyntaxKind as K;
988    node.children_with_tokens()
989        .filter_map(cstree::util::NodeOrToken::into_token)
990        .find_map(|t| {
991            Some(match t.kind() {
992                K::PlusEq => BinOp::Add,
993                K::MinusEq => BinOp::Sub,
994                K::StarEq => BinOp::Mul,
995                K::SlashEq => BinOp::Div,
996                K::PercentEq => BinOp::Mod,
997                K::StarStarEq => BinOp::Pow,
998                K::AmpEq => BinOp::BitAnd,
999                K::PipeEq => BinOp::BitOr,
1000                K::CaretEq => BinOp::BitXor,
1001                K::ShlEq => BinOp::Shl,
1002                K::ShrEq => BinOp::Shr,
1003                _ => return None,
1004            })
1005        })
1006}
1007
1008/// The prefix operator token of a `UnaryExpr`.
1009fn un_op(node: &GdNode) -> Option<UnOp> {
1010    node.children_with_tokens()
1011        .filter_map(cstree::util::NodeOrToken::into_token)
1012        .find_map(|t| UnOp::from_token(t.kind()))
1013}
1014
1015/// The member name + its range from a `FieldExpr` (the `NameRef` after the `.`).
1016fn field_member(node: &GdNode) -> Option<(SmolStr, TextRange)> {
1017    let nameref = cst::children_of(node, SyntaxKind::NameRef).pop()?;
1018    let tok = cst::first_token(&nameref)?;
1019    Some((SmolStr::new(tok.text()), cst::token_range(&tok)))
1020}
1021
1022/// The literal path of a `get_node("…")` / `get_node_or_null("…")` call (a **bare** call = implicit
1023/// `self.get_node`), or `None` if it isn't such a call or the argument is computed (the latter stays
1024/// a normal call → `Node`). Lets the call lower to a [`Expr::GetNode`] so it types like `$path`.
1025fn get_node_call_path(node: &GdNode) -> Option<SmolStr> {
1026    let callee = cst::first_child_expr(node)?;
1027    // The callee must be `get_node`/`get_node_or_null`, either **bare** (implicit `self`) or
1028    // **`self.<m>`** (explicit self = the same attach node). A *foreign* receiver
1029    // (`obj.get_node(...)`) is left as a normal call — its path is relative to another node we can't
1030    // resolve here.
1031    let is_get_node = match callee.kind() {
1032        SyntaxKind::NameRef => {
1033            cst::first_token(&callee).is_some_and(|t| is_get_node_name(t.text()))
1034        }
1035        SyntaxKind::FieldExpr => {
1036            is_self_receiver(&callee)
1037                && field_member(&callee).is_some_and(|(name, _)| is_get_node_name(&name))
1038        }
1039        _ => false,
1040    };
1041    if !is_get_node {
1042        return None;
1043    }
1044    let arg = cst::first_child(node, |k| k == SyntaxKind::ArgList)
1045        .and_then(|al| cst::first_child_expr(&al))?;
1046    if arg.kind() != SyntaxKind::Literal {
1047        return None; // computed `get_node(var)` — stays a normal call (→ Node)
1048    }
1049    let s = cst::child_token_text(&arg, SyntaxKind::String)?;
1050    Some(SmolStr::new(s.trim_matches(['"', '\''])))
1051}
1052
1053fn is_get_node_name(name: &str) -> bool {
1054    matches!(name, "get_node" | "get_node_or_null")
1055}
1056
1057/// Whether a `FieldExpr`'s receiver is `self` (a `NameRef` carrying a `SelfKw` token).
1058fn is_self_receiver(field_expr: &GdNode) -> bool {
1059    cst::first_child_expr(field_expr).is_some_and(|recv| {
1060        recv.kind() == SyntaxKind::NameRef
1061            && recv
1062                .children_with_tokens()
1063                .filter_map(cstree::util::NodeOrToken::into_token)
1064                .any(|t| t.kind() == SyntaxKind::SelfKw)
1065    })
1066}
1067
1068/// The literal node path from a `$Path`/`%Unique` (`GetNodeExpr`/`UniqueNodeExpr`) node: a dequoted
1069/// `$"a/b"` string, or the `/`-joined `Ident` segments of `$a/b`. `None` if it carries no path.
1070fn node_path_text(node: &GdNode) -> Option<SmolStr> {
1071    if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
1072        return Some(SmolStr::new(s.trim_matches(['"', '\''])));
1073    }
1074    let segs: Vec<String> = node
1075        .children_with_tokens()
1076        .filter_map(cstree::util::NodeOrToken::into_token)
1077        .filter(|t| t.kind() == SyntaxKind::Ident)
1078        .map(|t| t.text().to_owned())
1079        .collect();
1080    (!segs.is_empty()).then(|| SmolStr::new(segs.join("/")))
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085    use super::*;
1086    use gdscript_syntax::parse;
1087
1088    fn func_body(src: &str) -> Body {
1089        let root = parse(src).syntax_node();
1090        let func = gdscript_syntax::ast::descendants(&root)
1091            .into_iter()
1092            .find(|n| n.kind() == SyntaxKind::FuncDecl)
1093            .expect("a FuncDecl");
1094        body_of_func(&func)
1095    }
1096
1097    #[test]
1098    fn lowers_params_and_return() {
1099        let body = func_body("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
1100        assert_eq!(body.params.len(), 2);
1101        assert_eq!(body.params[0].name, "a");
1102        assert!(body.params[0].type_ref.is_some());
1103        assert!(body.params[1].default.is_some());
1104        assert_eq!(body.block.len(), 1);
1105        let Stmt::Return(Some(ret)) = body.stmt(body.block[0]) else {
1106            panic!("expected return")
1107        };
1108        assert!(matches!(body.expr(*ret), Expr::Bin { op: BinOp::Add, .. }));
1109    }
1110
1111    #[test]
1112    fn lowers_local_var_and_field_and_call() {
1113        let body = func_body("func f():\n\tvar n := get_node(\"x\")\n\tn.show()\n");
1114        // local var
1115        let Stmt::Var(v) = body.stmt(body.block[0]) else {
1116            panic!("expected var")
1117        };
1118        assert_eq!(v.name, "n");
1119        assert!(v.is_inferred && v.init.is_some());
1120        // n.show() — a call on a field
1121        let Stmt::Expr(e) = body.stmt(body.block[1]) else {
1122            panic!("expected expr stmt")
1123        };
1124        let Expr::Call { callee, .. } = body.expr(*e) else {
1125            panic!("expected call")
1126        };
1127        assert!(matches!(body.expr(*callee), Expr::Field { name, .. } if name == "show"));
1128    }
1129
1130    #[test]
1131    fn lowers_if_with_is_narrowing() {
1132        let body = func_body("func f(x):\n\tif x is Node:\n\t\tx.free()\n\telse:\n\t\tpass\n");
1133        let Stmt::If {
1134            cond,
1135            then_branch,
1136            else_branch,
1137            ..
1138        } = body.stmt(body.block[0])
1139        else {
1140            panic!("expected if")
1141        };
1142        assert!(matches!(body.expr(*cond), Expr::Is { negated: false, .. }));
1143        assert_eq!(then_branch.len(), 1);
1144        assert!(else_branch.is_some());
1145    }
1146
1147    #[test]
1148    fn source_map_finds_tightest_expr() {
1149        // `a + b` — offset on `b` should resolve to the Name(b) expr, not the whole BinExpr.
1150        let body = func_body("func f(a, b):\n\treturn a + b\n");
1151        let b_offset = u32::try_from("func f(a, b):\n\treturn a + ".len()).unwrap();
1152        let id = body
1153            .source_map
1154            .expr_at_offset(b_offset)
1155            .expect("an expr at b");
1156        assert!(matches!(body.expr(id), Expr::Name(n) if n == "b"));
1157    }
1158
1159    #[test]
1160    fn initializer_body_has_tail() {
1161        let root = parse("var x = 1 + 2\n").syntax_node();
1162        let var = gdscript_syntax::ast::descendants(&root)
1163            .into_iter()
1164            .find(|n| n.kind() == SyntaxKind::VarDecl)
1165            .unwrap();
1166        let init = crate::cst::first_child_expr(&var).unwrap();
1167        let body = body_of_expr(&init);
1168        assert!(body.tail.is_some());
1169        assert!(matches!(
1170            body.expr(body.tail.unwrap()),
1171            Expr::Bin { op: BinOp::Add, .. }
1172        ));
1173    }
1174}