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