Skip to main content

gdscript_hir/
infer.rs

1//! Gradual type inference (Playbook §3.3–§3.6 + §5): a single forward, bottom-up,
2//! bidirectional walk over a lowered [`Body`]. No unification variables — types flow forward
3//! from annotations, literals, and the engine API (rust-analyzer's *structure*, Pyright's
4//! gradual *semantics*).
5//!
6//! The walk memoizes every expression's [`Ty`] in [`InferenceResult::expr_ty`] (the source of
7//! hover + inlay), does flow-scoped `is`/`as` narrowing over the lexical guarded sub-tree, and
8//! raises the §5 type diagnostics. The load-bearing invariant: a `Variant`/`Unknown`/`Error`
9//! receiver is *uninformative* — it never fires `UNSAFE_*`, never cascades — so cross-file code
10//! (which lands on `Unknown` via the seam) produces zero false diagnostics.
11
12use gdscript_api::{EngineApi, MemberRef, TyRef};
13use gdscript_base::{Diagnostic, DiagnosticSource, FileId, Severity, TextRange};
14use gdscript_db::Db;
15use gdscript_scene::{SceneModel, SceneNode};
16use gdscript_syntax::GdNode;
17use rustc_hash::FxHashMap;
18use smol_str::SmolStr;
19
20use std::sync::Arc;
21
22use crate::body::{self, BinOp, Body, Expr, ExprId, Literal, ParamBinding, Stmt, UnOp};
23use crate::cst::{self, AstPtr};
24use crate::item_tree::{ItemTree, Member, item_tree};
25use crate::resolve::{self, ClassItem, ClassScope, GlobalDef};
26use crate::ty::{self, Assign, EnumRef, ScriptRefId, Ty};
27
28// ---- diagnostic codes + message templates (Playbook §5, engine-matching) -----------------
29
30/// `:=` / inferred binding from a statically-`Variant` value.
31pub const INFERENCE_ON_VARIANT: &str = "INFERENCE_ON_VARIANT";
32/// Incompatible hard types (our umbrella for the engine's `push_error`).
33pub const TYPE_MISMATCH: &str = "TYPE_MISMATCH";
34/// `float` stored into an `int` slot.
35pub const NARROWING_CONVERSION: &str = "NARROWING_CONVERSION";
36/// `int / int`.
37pub const INTEGER_DIVISION: &str = "INTEGER_DIVISION";
38/// A property missing on a statically-known base.
39pub const UNSAFE_PROPERTY_ACCESS: &str = "UNSAFE_PROPERTY_ACCESS";
40/// A method missing on a statically-known base.
41pub const UNSAFE_METHOD_ACCESS: &str = "UNSAFE_METHOD_ACCESS";
42/// A `$Path`/`%Unique`/`get_node("…")` whose literal path is genuinely absent in the owning scene
43/// (only raised when the script attaches to exactly one scene — never on an `..`/absolute path or a
44/// path that descends into an instanced sub-scene we don't see).
45pub const INVALID_NODE_PATH: &str = "INVALID_NODE_PATH";
46
47/// What kind of binding a [`Binding`] describes.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum BindingKind {
50    /// A local `var` / `const`.
51    Var,
52    /// A function / lambda parameter.
53    Param,
54    /// A `for` loop variable.
55    ForVar,
56    /// A `var x` capture in a `match` pattern (typed `Variant`; arm-scoped).
57    MatchBind,
58}
59
60/// A typed local binding — the unit hover + inlay hints read for `var`/param/`for` names.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Binding {
63    /// The name token's range.
64    pub name_range: TextRange,
65    /// The binding's resolved type. For an untyped `var x = e` this is the gradual `Variant`;
66    /// the precise initializer type (for an "add type annotation" action) is [`Binding::init`].
67    pub ty: Ty,
68    /// The initializer expression, when the binding has one (a `var`/`const` with `= e`).
69    pub init: Option<ExprId>,
70    /// Whether the source carried an explicit `: T` annotation.
71    pub annotated: bool,
72    /// Whether the source used `:=` (inferred-but-hard).
73    pub inferred_colon_eq: bool,
74    /// What kind of binding this is.
75    pub kind: BindingKind,
76}
77
78/// The result of inferring one body.
79#[derive(Debug, Clone, Default, PartialEq, Eq)]
80pub struct InferenceResult {
81    /// Every expression's inferred type (feeds hover + inlay).
82    pub expr_ty: FxHashMap<ExprId, Ty>,
83    /// The local bindings introduced by the body (params, `var`/`const`, `for` vars).
84    pub bindings: Vec<Binding>,
85    /// The §5 type diagnostics raised.
86    pub diagnostics: Vec<Diagnostic>,
87}
88
89impl InferenceResult {
90    /// The inferred type of an expression, if it was visited.
91    #[must_use]
92    pub fn type_of(&self, id: ExprId) -> Option<&Ty> {
93        self.expr_ty.get(&id)
94    }
95
96    /// The binding whose name token contains `offset`, if any.
97    #[must_use]
98    pub fn binding_at(&self, offset: u32) -> Option<&Binding> {
99        self.bindings
100            .iter()
101            .find(|b| b.name_range.start <= offset && offset < b.name_range.end)
102    }
103}
104
105/// Infer a lowered `body` (its `tail` initializer expression and/or its statement block).
106/// `return_ty` is the function's declared return type (`Variant` if none / for an
107/// initializer body).
108#[must_use]
109pub fn infer(
110    db: &dyn Db,
111    api: &EngineApi,
112    root: &GdNode,
113    class: &ClassScope,
114    body: &Body,
115    return_ty: Ty,
116) -> InferenceResult {
117    let self_ty = class.self_ty.clone();
118    let mut cx = Cx {
119        db,
120        api,
121        root,
122        body,
123        class,
124        self_ty,
125        return_ty,
126        expr_ty: FxHashMap::default(),
127        bindings: Vec::new(),
128        diagnostics: Vec::new(),
129        locals: FxHashMap::default(),
130        narrowing: FxHashMap::default(),
131    };
132    // Parameters bind first (their defaults can reference earlier params).
133    let params = body.params.clone();
134    for p in &params {
135        let ty = cx.param_ty(p);
136        cx.bindings.push(Binding {
137            name_range: p.name_range,
138            ty: ty.clone(),
139            init: None,
140            annotated: p.type_ref.is_some(),
141            inferred_colon_eq: false,
142            kind: BindingKind::Param,
143        });
144        cx.locals.insert(p.name.clone(), ty);
145    }
146    if let Some(tail) = body.tail {
147        cx.infer_expr(tail, &Expectation::None);
148    }
149    let block = body.block.clone();
150    cx.infer_block(&block);
151    InferenceResult {
152        expr_ty: cx.expr_ty,
153        bindings: cx.bindings,
154        diagnostics: cx.diagnostics,
155    }
156}
157
158/// Convenience: recover a function node from its [`AstPtr`], lower its body, resolve its
159/// declared return type, and infer it.
160#[must_use]
161pub fn infer_func(
162    db: &dyn Db,
163    api: &EngineApi,
164    root: &GdNode,
165    class: &ClassScope,
166    ptr: AstPtr,
167) -> InferenceResult {
168    let Some(node) = ptr.to_node(root) else {
169        return InferenceResult::default();
170    };
171    let body = body::body_of_func(&node);
172    // The return-type annotation is the FuncDecl's direct `TypeRef` child (params' type refs
173    // are nested inside the ParamList, so they are not direct children).
174    let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
175        .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
176    infer(db, api, root, class, &body, return_ty)
177}
178
179/// One inferred unit of a file: a function body or a class field's initializer, with its
180/// lowered [`Body`] and [`InferenceResult`] (kept so position-based features — hover, inlay,
181/// member completion — can map a cursor back through the source map).
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Unit {
184    /// The source range this unit covers (the function decl or the field decl).
185    pub range: TextRange,
186    /// The lowered body.
187    pub body: Body,
188    /// The inference result.
189    pub result: InferenceResult,
190}
191
192/// The full single-file inference: the item tree, every inferred unit, and the merged §5
193/// diagnostics. The whole-file entry point the IDE layer consumes.
194#[derive(Debug, Clone, PartialEq, Eq, Default)]
195pub struct FileInference {
196    /// The lowered item tree.
197    pub tree: Arc<ItemTree>,
198    /// The inferred function/field units.
199    pub units: Vec<Unit>,
200    /// All type diagnostics, merged across units.
201    pub diagnostics: Vec<Diagnostic>,
202}
203
204impl FileInference {
205    /// The innermost unit whose range contains `offset`.
206    #[must_use]
207    pub fn unit_at(&self, offset: u32) -> Option<&Unit> {
208        self.units
209            .iter()
210            .filter(|u| u.range.start <= offset && offset < u.range.end)
211            .min_by_key(|u| u.range.end - u.range.start)
212    }
213}
214
215/// Infer an entire file: lower its item tree, then infer every function body and every
216/// class-field initializer against a shared [`ClassScope`]. The single entry point for the
217/// IDE features (Playbook §6 — a pure `(api, parsed file) -> result` function).
218#[must_use]
219pub fn analyze_file(db: &dyn Db, api: &EngineApi, root: &GdNode, file_id: FileId) -> FileInference {
220    let tree = item_tree(root);
221    let mut units = Vec::new();
222    let mut diagnostics = Vec::new();
223    let mut member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
224    // `self` is the script's OWN class (a self-`ScriptRef`), not just its engine base — so member
225    // access on an aliased `self` resolves the file's own members (see `ClassScope::self_ty`).
226    let self_ref = Ty::ScriptRef(ScriptRefId(file_id.0));
227
228    // Pass 1 — class fields. Inferring each `var`/`const` seeds `member_types` so the function
229    // pass sees the *inferred* field type (`var n := 0` → `int`), not just the annotation.
230    {
231        let mut class = ClassScope::new(db, api, &tree);
232        class.self_ty = self_ref.clone();
233        for m in &tree.members {
234            let (ptr, range) = match m {
235                Member::Var(v) => (v.ptr, v.range),
236                Member::Const(c) => (c.ptr, c.range),
237                _ => continue,
238            };
239            if let Some(unit) = unit_from_decl(db, api, root, &class, ptr, range) {
240                if let (Some(name), Some(b)) = (m.name(), unit.result.bindings.first()) {
241                    member_types.insert(SmolStr::new(name), b.ty.clone());
242                }
243                diagnostics.extend(unit.result.diagnostics.iter().cloned());
244                units.push(unit);
245            }
246        }
247    }
248
249    // Pass 2 — functions, against a scope carrying the seeded field types.
250    {
251        let mut class = ClassScope::new(db, api, &tree);
252        class.member_types = member_types;
253        class.self_ty = self_ref.clone();
254        for m in &tree.members {
255            let Member::Func(f) = m else { continue };
256            let Some(node) = f.ptr.to_node(root) else {
257                continue;
258            };
259            let body = body::body_of_func(&node);
260            let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
261                .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
262            let result = infer(db, api, root, &class, &body, return_ty);
263            diagnostics.extend(result.diagnostics.iter().cloned());
264            units.push(Unit {
265                range: f.range,
266                body,
267                result,
268            });
269        }
270    }
271
272    FileInference {
273        tree,
274        units,
275        diagnostics,
276    }
277}
278
279/// Infer a class field declaration as a single local-var statement (full annotation checks).
280fn unit_from_decl(
281    db: &dyn Db,
282    api: &EngineApi,
283    root: &GdNode,
284    class: &ClassScope,
285    ptr: AstPtr,
286    range: TextRange,
287) -> Option<Unit> {
288    let node = ptr.to_node(root)?;
289    let body = body::body_of_decl_stmt(&node);
290    let result = infer(db, api, root, class, &body, Ty::Variant);
291    Some(Unit {
292        range,
293        body,
294        result,
295    })
296}
297
298/// What type is expected of an expression (bidirectional checking).
299enum Expectation {
300    /// No expectation — pure synthesis.
301    None,
302    /// The expression is checked against this declared type.
303    Has(Ty),
304}
305
306struct Cx<'a> {
307    db: &'a dyn Db,
308    api: &'a EngineApi,
309    root: &'a GdNode,
310    body: &'a Body,
311    class: &'a ClassScope<'a>,
312    self_ty: Ty,
313    return_ty: Ty,
314    expr_ty: FxHashMap<ExprId, Ty>,
315    bindings: Vec<Binding>,
316    diagnostics: Vec<Diagnostic>,
317    /// Function-scoped local bindings (GDScript locals are function-, not block-, scoped).
318    locals: FxHashMap<SmolStr, Ty>,
319    /// Flow-scoped `is`/`as` narrowing facts, keyed by a dotted access path.
320    narrowing: FxHashMap<String, Ty>,
321}
322
323impl Cx<'_> {
324    // ---- small type constructors ----
325
326    fn builtin(&self, name: &str) -> Ty {
327        self.api
328            .builtin_by_name(name)
329            .map_or(Ty::Variant, Ty::Builtin)
330    }
331    fn int_ty(&self) -> Ty {
332        self.builtin("int")
333    }
334    fn float_ty(&self) -> Ty {
335        self.builtin("float")
336    }
337    fn bool_ty(&self) -> Ty {
338        self.builtin("bool")
339    }
340    fn is_int(&self, ty: &Ty) -> bool {
341        matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "int")
342    }
343    fn is_float(&self, ty: &Ty) -> bool {
344        matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "float")
345    }
346    fn is_numeric(&self, ty: &Ty) -> bool {
347        self.is_int(ty) || self.is_float(ty)
348    }
349
350    // ---- diagnostics ----
351
352    fn emit(&mut self, range: TextRange, severity: Severity, code: &str, message: String) {
353        self.diagnostics.push(Diagnostic {
354            range,
355            severity,
356            code: code.to_owned(),
357            message,
358            source: DiagnosticSource::Type,
359            fixes: Vec::new(),
360        });
361    }
362
363    fn range_of(&self, id: ExprId) -> TextRange {
364        self.body.source_map.expr_range(id)
365    }
366
367    /// Run `is_assignable(from, to)` and raise the matching diagnostic. Safe to call
368    /// unconditionally: `to` being `Variant`/`Unknown` yields `Ok`/no diagnostic.
369    fn check_assign(&mut self, from: &Ty, to: &Ty, range: TextRange) {
370        match ty::is_assignable(self.api, from, to) {
371            Assign::Narrowing => self.emit(
372                range,
373                Severity::Warning,
374                NARROWING_CONVERSION,
375                "Narrowing conversion (float is converted to int and loses precision).".to_owned(),
376            ),
377            Assign::No => {
378                let to_label = to.label(self.api).unwrap_or_else(|| "?".to_owned());
379                let from_label = from.label(self.api).unwrap_or_else(|| "?".to_owned());
380                self.emit(
381                    range,
382                    Severity::Error,
383                    TYPE_MISMATCH,
384                    format!(
385                        "Cannot assign a value of type \"{from_label}\" to a target of type \"{to_label}\"."
386                    ),
387                );
388            }
389            Assign::Ok | Assign::OkUnsafe | Assign::IntAsEnum => {}
390        }
391    }
392
393    // ---- statements ----
394
395    fn infer_block(&mut self, block: &[body::StmtId]) {
396        for &stmt in block {
397            self.infer_stmt(stmt);
398        }
399    }
400
401    fn infer_stmt(&mut self, id: body::StmtId) {
402        match self.body.stmt(id).clone() {
403            Stmt::Expr(e) => {
404                self.infer_expr(e, &Expectation::None);
405            }
406            Stmt::Var(v) => self.infer_local_var(&v),
407            Stmt::Return(e) => {
408                if let Some(e) = e {
409                    let expected = if self.return_ty.is_uninformative() {
410                        Expectation::None
411                    } else {
412                        Expectation::Has(self.return_ty.clone())
413                    };
414                    let t = self.infer_expr(e, &expected);
415                    if let Expectation::Has(ret) = expected {
416                        self.check_assign(&t, &ret, self.range_of(e));
417                    }
418                }
419            }
420            Stmt::If {
421                cond,
422                then_branch,
423                elifs,
424                else_branch,
425            } => {
426                self.infer_expr(cond, &Expectation::None);
427                self.in_branch(|cx| {
428                    cx.apply_narrowing(cond);
429                    cx.infer_block(&then_branch);
430                });
431                for (econd, eblock) in elifs {
432                    self.infer_expr(econd, &Expectation::None);
433                    self.in_branch(|cx| {
434                        cx.apply_narrowing(econd);
435                        cx.infer_block(&eblock);
436                    });
437                }
438                if let Some(eb) = else_branch {
439                    self.in_branch(|cx| cx.infer_block(&eb));
440                }
441            }
442            Stmt::While { cond, body } => {
443                self.infer_expr(cond, &Expectation::None);
444                self.in_branch(|cx| cx.infer_block(&body));
445            }
446            Stmt::For(f) => {
447                let iter_ty = self.infer_expr(f.iter, &Expectation::None);
448                let var_ty = f.var_type.as_ref().map_or_else(
449                    || self.loop_var_ty(&iter_ty),
450                    |ptr| self.resolve_ptr_ty(*ptr),
451                );
452                self.bindings.push(Binding {
453                    name_range: f.var_range,
454                    ty: var_ty.clone(),
455                    init: None,
456                    annotated: f.var_type.is_some(),
457                    inferred_colon_eq: false,
458                    kind: BindingKind::ForVar,
459                });
460                self.locals.insert(f.var.clone(), var_ty);
461                self.in_branch(|cx| cx.infer_block(&f.body));
462            }
463            Stmt::Match { scrutinee, arms } => {
464                self.infer_expr(scrutinee, &Expectation::None);
465                for arm in arms {
466                    self.in_branch(|cx| {
467                        for b in &arm.binds {
468                            // Record the capture as a binding so navigation (find-refs / rename)
469                            // sees it as a local that shadows a same-named member; the type is the
470                            // Phase-2 `Variant`.
471                            cx.bindings.push(Binding {
472                                name_range: b.range,
473                                ty: Ty::Variant,
474                                init: None,
475                                annotated: false,
476                                inferred_colon_eq: false,
477                                kind: BindingKind::MatchBind,
478                            });
479                            cx.locals.insert(b.name.clone(), Ty::Variant);
480                        }
481                        if let Some(g) = arm.guard {
482                            cx.infer_expr(g, &Expectation::None);
483                        }
484                        cx.infer_block(&arm.body);
485                    });
486                }
487            }
488            Stmt::Break | Stmt::Continue | Stmt::Pass => {}
489            Stmt::Assert(cond) => {
490                if let Some(cond) = cond {
491                    self.infer_expr(cond, &Expectation::None);
492                }
493            }
494        }
495    }
496
497    fn infer_local_var(&mut self, v: &body::LocalVar) {
498        let annotated = v.type_ref.map(|p| self.resolve_ptr_ty(p));
499        let init_ty = v.init.map(|e| {
500            let expected = annotated
501                .as_ref()
502                .map_or(Expectation::None, |t| Expectation::Has(t.clone()));
503            self.infer_expr(e, &expected)
504        });
505        let range = v.init.map_or(v.name_range, |e| self.range_of(e));
506
507        let binding_ty = match (&annotated, &init_ty) {
508            // `var x: T = e` — hard slot; check the initializer against it.
509            (Some(t), Some(init)) => {
510                self.check_assign(init, t, range);
511                t.clone()
512            }
513            // `var x: T` (no init).
514            (Some(t), None) => t.clone(),
515            // `var x := e` — inferred (hard); guard the Variant / null cases.
516            (None, Some(init)) if v.is_inferred => {
517                if init.is_variant() {
518                    self.emit(
519                        range,
520                        Severity::Error,
521                        INFERENCE_ON_VARIANT,
522                        inference_on_variant_msg(if v.is_const { "constant" } else { "variable" }),
523                    );
524                    Ty::Variant
525                } else {
526                    // `Unknown` (the seam) stays `Unknown` with no warning.
527                    init.clone()
528                }
529            }
530            // `var x = e` — untyped, soft → Variant. `const X = e` keeps the inferred type.
531            (None, Some(init)) => {
532                if v.is_const {
533                    init.clone()
534                } else {
535                    Ty::Variant
536                }
537            }
538            (None, None) => Ty::Variant,
539        };
540        self.bindings.push(Binding {
541            name_range: v.name_range,
542            ty: binding_ty.clone(),
543            init: v.init,
544            annotated: v.type_ref.is_some(),
545            inferred_colon_eq: v.is_inferred,
546            kind: BindingKind::Var,
547        });
548        self.narrowing.remove(v.name.as_str());
549        self.locals.insert(v.name.clone(), binding_ty);
550    }
551
552    // ---- expressions ----
553
554    fn infer_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
555        let ty = self.synth_expr(id, expected);
556        self.expr_ty.insert(id, ty.clone());
557        ty
558    }
559
560    #[allow(clippy::too_many_lines)]
561    fn synth_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
562        match self.body.expr(id).clone() {
563            Expr::Missing => Ty::Error,
564            Expr::Literal(lit) => self.literal_ty(lit),
565            Expr::Name(name) => self.resolve_name(id, &name),
566            Expr::SelfExpr => self.self_ty.clone(),
567            Expr::Super => self.class.base.clone(),
568            Expr::Paren(inner) => self.infer_expr(inner, expected),
569            Expr::Bin { op, lhs, rhs } => self.infer_bin(id, op, lhs, rhs),
570            Expr::Unary { op, operand } => {
571                let t = self.infer_expr(operand, &Expectation::None);
572                match op {
573                    UnOp::Not => self.bool_ty(),
574                    UnOp::BitNot => self.int_ty(),
575                    UnOp::Neg | UnOp::Pos => {
576                        if t.is_uninformative() || self.is_numeric(&t) {
577                            t
578                        } else {
579                            Ty::Variant
580                        }
581                    }
582                }
583            }
584            Expr::Ternary {
585                cond,
586                then_branch,
587                else_branch,
588            } => {
589                self.infer_expr(cond, &Expectation::None);
590                let a = self.infer_expr(then_branch, expected);
591                let b = self.infer_expr(else_branch, expected);
592                // A `null` branch does not poison the other: `x if c else null` is nullable-`x`.
593                if self.is_null(else_branch) {
594                    a
595                } else if self.is_null(then_branch) {
596                    b
597                } else {
598                    self.join(&a, &b)
599                }
600            }
601            Expr::Call { callee, args } => self.infer_call(callee, &args),
602            Expr::Field {
603                receiver,
604                name,
605                name_range,
606            } => {
607                self.infer_field(receiver, &name, name_range, /*as_method=*/ false)
608            }
609            Expr::Index { base, index } => {
610                let base_ty = self.infer_expr(base, &Expectation::None);
611                self.infer_expr(index, &Expectation::None);
612                self.index_ty(&base_ty)
613            }
614            Expr::Is { operand, .. } => {
615                self.infer_expr(operand, &Expectation::None);
616                self.bool_ty()
617            }
618            Expr::Cast { operand, ty } => {
619                self.infer_expr(operand, &Expectation::None);
620                ty.map_or(Ty::Variant, |p| self.resolve_ptr_ty(p))
621            }
622            Expr::In { lhs, rhs, .. } => {
623                self.infer_expr(lhs, &Expectation::None);
624                self.infer_expr(rhs, &Expectation::None);
625                self.bool_ty()
626            }
627            Expr::Await(operand) => {
628                self.infer_expr(operand, &Expectation::None);
629                // The awaited result (a signal's args / a coroutine's return) is not tracked in
630                // Phase 2 — use the seam, not `Variant`, so `var x := await f()` never warns.
631                Ty::Unknown
632            }
633            Expr::Array(elems) => {
634                // Checking mode: an expected `Array[T]` is pushed down onto the literal (so
635                // `var a: Array[String] = []` / `[...]` is accepted). Otherwise the engine does
636                // not infer a literal's element type past `Variant`.
637                let pushed = match expected {
638                    Expectation::Has(Ty::Array(e)) => Some((**e).clone()),
639                    _ => None,
640                };
641                let elem_exp = pushed.clone().map_or(Expectation::None, Expectation::Has);
642                for e in elems {
643                    self.infer_expr(e, &elem_exp);
644                }
645                pushed.map_or_else(Ty::array_of_variant, |e| Ty::Array(Box::new(e)))
646            }
647            Expr::Dict(entries) => {
648                let pushed = match expected {
649                    Expectation::Has(Ty::Dict(k, v)) => Some(((**k).clone(), (**v).clone())),
650                    _ => None,
651                };
652                let (kx, vx) = pushed
653                    .clone()
654                    .map_or((Expectation::None, Expectation::None), |(k, v)| {
655                        (Expectation::Has(k), Expectation::Has(v))
656                    });
657                for (k, v) in entries {
658                    self.infer_expr(k, &kx);
659                    if let Some(v) = v {
660                        self.infer_expr(v, &vx);
661                    }
662                }
663                pushed.map_or_else(Ty::dict_of_variant, |(k, v)| {
664                    Ty::Dict(Box::new(k), Box::new(v))
665                })
666            }
667            Expr::Lambda { params, body } => {
668                self.infer_lambda(&params, &body);
669                Ty::Callable
670            }
671            Expr::Preload { arg, path } => {
672                if let Some(arg) = arg {
673                    self.infer_expr(arg, &Expectation::None);
674                }
675                // A constant string-literal path resolves to the declaring file's `ScriptRef`
676                // (M3 — a SCRIPT meta-type in Godot; `X.new()`/`X.member` then resolve via the
677                // usual `ScriptRef` walk). A non-constant argument (`preload(var)`) — which Godot
678                // itself rejects — stays the seam, never a false diagnostic.
679                match path {
680                    Some(p) => {
681                        resolve::resolve_external(self.db, &resolve::ExternalRef::Preload(p))
682                    }
683                    None => Ty::Unknown,
684                }
685            }
686            // `$Path`/`%Unique` — resolve the literal path against the owning scene to the node's
687            // concrete type (Phase-4 M1); a computed/unresolvable path stays `Object(Node)`.
688            Expr::GetNode { path, unique } => self.resolve_node_path(id, path.as_deref(), unique),
689        }
690    }
691
692    /// Whether `id` is the `null` literal.
693    fn is_null(&self, id: ExprId) -> bool {
694        matches!(self.body.expr(id), Expr::Literal(Literal::Null))
695    }
696
697    fn literal_ty(&self, lit: Literal) -> Ty {
698        match lit {
699            Literal::Int => self.int_ty(),
700            Literal::Float | Literal::MathConst => self.float_ty(),
701            Literal::Bool => self.bool_ty(),
702            Literal::Str => self.builtin("String"),
703            Literal::StringName => self.builtin("StringName"),
704            Literal::NodePath => self.builtin("NodePath"),
705            // `null` is compatible everywhere; typing it `Variant` avoids false mismatches.
706            Literal::Null => Ty::Variant,
707        }
708    }
709
710    fn node_ty(&self) -> Ty {
711        self.api
712            .class_by_name("Node")
713            .map_or(Ty::Unknown, Ty::Object)
714    }
715
716    // ---- scene-aware node-path typing (Phase-4 M1) ----
717
718    /// Resolve a `$Path`/`%Unique`/`get_node("…")` literal node path against the owning scene to the
719    /// node's concrete type. A computed (`None`) path, no owning scene, an `..`/absolute escape, or a
720    /// path that descends into an instanced sub-scene all degrade to `Object(Node)` — never a false
721    /// positive. A *genuinely* absent in-scene node raises `INVALID_NODE_PATH` (M2), but only when
722    /// the script attaches to exactly one scene (an ambiguous multi-scene attachment stays silent).
723    fn resolve_node_path(&mut self, id: ExprId, path: Option<&str>, unique: bool) -> Ty {
724        use gdscript_scene::NodePathResolution as R;
725        let fallback = self.node_ty();
726        let Some(path) = path else {
727            return fallback; // computed `get_node(var)` — stays `Node`
728        };
729        let Some(ctx) = self.owning_scene() else {
730            return fallback; // no scene attaches this script (dynamic UI / single-file)
731        };
732        let resolution = if unique {
733            ctx.model.classify_unique(path)
734        } else {
735            ctx.model.classify_path_from(ctx.attach, path)
736        };
737        match resolution {
738            R::Resolved(idx) => ctx
739                .model
740                .node(idx)
741                .and_then(|n| self.scene_node_ty(&ctx.model, n, 0))
742                .unwrap_or(fallback),
743            R::Missing if !ctx.ambiguous => {
744                let what = if unique { "unique name" } else { "node path" };
745                let sigil = if unique { "%" } else { "$" };
746                self.emit(
747                    self.range_of(id),
748                    Severity::Warning,
749                    INVALID_NODE_PATH,
750                    format!("no {what} `{sigil}{path}` in the owning scene"),
751                );
752                fallback
753            }
754            // ambiguous miss / escape (`..`/absolute) / into-instance → `Node`, never a false warning
755            _ => fallback,
756        }
757    }
758
759    /// The owning-scene context for the current file (scene + attach node + multi-scene ambiguity).
760    /// Recovered from `self_ty`, which `analyze_file` sets to the file's own `ScriptRef` (so no extra
761    /// `FileId` threading).
762    fn owning_scene(&self) -> Option<crate::queries::SceneContext> {
763        let Ty::ScriptRef(sref) = &self.self_ty else {
764            return None;
765        };
766        let ft = self.db.file_text(FileId(sref.0))?;
767        crate::queries::scene_context(self.db, ft)
768    }
769
770    /// The concrete `Ty` of a scene node, by precedence: an attached script's own class (most
771    /// specific) wins; else the declared `type=` (native class or `class_name`); else — an instanced
772    /// node (`instance=`, no own `type=`/script) — the **instanced sub-scene's root** type (M3,
773    /// recursive). `None` for a node we can't sharpen (the caller degrades to `Node`).
774    fn scene_node_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
775        if let Some(script_ty) = self.node_script_ref(scene, node) {
776            return Some(script_ty);
777        }
778        if let Some(decl) = node.decl_type.as_ref() {
779            let ty = resolve::resolve_type_name(self.db, self.api, decl);
780            if !ty.is_uninformative() {
781                return Some(ty);
782            }
783        }
784        self.instance_root_ty(scene, node, depth)
785    }
786
787    /// An instanced node (`instance=ExtResource(id)`) takes the type of the instanced sub-scene's
788    /// ROOT node — resolved recursively, so the root's own script / `type=` / nested instance all
789    /// flow through (so `$Enemy` types as `enemy.tscn`'s root class, not bare `Node`). Depth-bounded
790    /// against an instancing cycle (scene A instances B instances A).
791    fn instance_root_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
792        if depth >= 16 {
793            return None;
794        }
795        let inst = node.instance.as_ref()?;
796        let path = scene.ext_resources.get(inst)?.path.as_ref()?;
797        let root = self.db.source_root()?;
798        let file = crate::queries::res_path_registry(self.db, root)
799            .get(path.as_str())
800            .copied()?;
801        let ft = self.db.file_text(file)?;
802        let sub = crate::queries::scene_model(self.db, ft);
803        let root_node = sub.node(sub.root?)?;
804        self.scene_node_ty(&sub, root_node, depth + 1)
805    }
806
807    /// The `ScriptRef` of a node's attached `.gd` script (`script = ExtResource(id)` → its `res://`
808    /// path → `FileId`), or `None` if it has no resolvable external script.
809    fn node_script_ref(&self, scene: &SceneModel, node: &SceneNode) -> Option<Ty> {
810        let path = scene
811            .ext_resources
812            .get(node.script.as_ref()?)?
813            .path
814            .as_ref()?;
815        let root = self.db.source_root()?;
816        let file = crate::queries::res_path_registry(self.db, root)
817            .get(path.as_str())
818            .copied()?;
819        Some(Ty::ScriptRef(ScriptRefId(file.0)))
820    }
821
822    fn infer_bin(&mut self, id: ExprId, op: BinOp, lhs: ExprId, rhs: ExprId) -> Ty {
823        if op == BinOp::Assign {
824            return self.infer_assign(lhs, rhs);
825        }
826        let lt = self.infer_expr(lhs, &Expectation::None);
827        let rt = self.infer_expr(rhs, &Expectation::None);
828        if op.is_boolean() {
829            return self.bool_ty();
830        }
831        // `int / int` discards the fractional part.
832        if op == BinOp::Div && self.is_int(&lt) && self.is_int(&rt) {
833            self.emit(
834                self.range_of(id),
835                Severity::Warning,
836                INTEGER_DIVISION,
837                "Integer division. Decimal part will be discarded.".to_owned(),
838            );
839            return self.int_ty();
840        }
841        self.bin_result(op, &lt, &rt)
842    }
843
844    fn infer_assign(&mut self, lhs: ExprId, rhs: ExprId) -> Ty {
845        let slot = self.infer_expr(lhs, &Expectation::None);
846        let expected = if slot.is_uninformative() {
847            Expectation::None
848        } else {
849            Expectation::Has(slot.clone())
850        };
851        let value = self.infer_expr(rhs, &expected);
852        if !slot.is_uninformative() {
853            self.check_assign(&value, &slot, self.range_of(rhs));
854        }
855        // Assignment narrowing: bound the narrowed type by the declared slot.
856        if let Some(key) = self.narrow_key(lhs) {
857            let narrowed = if slot.is_uninformative() {
858                value.clone()
859            } else {
860                slot.clone()
861            };
862            self.narrowing.insert(key, narrowed);
863        }
864        slot
865    }
866
867    /// Resolve a binary operator's result type via the builtin operator table, with a numeric
868    /// fallback. Comparison/logical operators are handled by the caller.
869    fn bin_result(&self, op: BinOp, lt: &Ty, rt: &Ty) -> Ty {
870        if let (Ty::Builtin(b), Some(sym)) = (lt, op_symbol(op)) {
871            for o in self.api.builtin_operators(*b) {
872                if o.op == sym
873                    && let Some(right) = &o.right
874                    && self.tyref_matches(right, rt)
875                {
876                    return ty::resolve_tyref(self.api, &o.result);
877                }
878            }
879        }
880        if self.is_numeric(lt) && self.is_numeric(rt) {
881            return if self.is_float(lt) || self.is_float(rt) {
882                self.float_ty()
883            } else {
884                self.int_ty()
885            };
886        }
887        // A seam operand keeps the result on the seam (`a + unknown` is `Unknown`, not the
888        // gradual `Variant`, so `var x := a + unknown` never warns).
889        if lt.is_unknown() || rt.is_unknown() || lt.is_error() || rt.is_error() {
890            return Ty::Unknown;
891        }
892        Ty::Variant
893    }
894
895    fn tyref_matches(&self, tyref: &TyRef, ty: &Ty) -> bool {
896        let resolved = ty::resolve_tyref(self.api, tyref);
897        resolved.is_variant() || &resolved == ty
898    }
899
900    fn infer_call(&mut self, callee: ExprId, args: &[ExprId]) -> Ty {
901        // Argument expressions are always inferred (their own diagnostics + hover).
902        for &a in args {
903            self.infer_expr(a, &Expectation::None);
904        }
905        match self.body.expr(callee).clone() {
906            Expr::Field {
907                receiver,
908                name,
909                name_range,
910            } => {
911                self.infer_field(receiver, &name, name_range, /*as_method=*/ true)
912            }
913            Expr::Name(name) => {
914                let ret = self.resolve_call_name(&name);
915                self.expr_ty.insert(callee, Ty::Callable);
916                ret
917            }
918            // Calling an arbitrary expression — a `Callable` value or an immediately-invoked
919            // lambda (`(func(): …).call()`): the callee's return type isn't tracked, so the
920            // result is the seam (not `Variant`), and `var x := f()()` never warns.
921            _ => {
922                self.infer_expr(callee, &Expectation::None);
923                Ty::Unknown
924            }
925        }
926    }
927
928    /// Resolve a bare-name call (`foo(...)`): own method → utility/builtin fn → constructor.
929    fn resolve_call_name(&self, name: &str) -> Ty {
930        if let Some(item) = self.class.lookup(name)
931            && let Some(Member::Func(f)) = self.class.member(item)
932        {
933            return self.func_return_ty(f.return_type.as_deref());
934        }
935        // A bare call inside the class is `self.name(...)` — resolve against the inherited base.
936        if let Ty::Object(base) = self.class.base
937            && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
938        {
939            return ty::resolve_tyref(self.api, &sig.return_ty);
940        }
941        if let Some(u) = self.api.utility(name) {
942            return ty::resolve_tyref(self.api, &u.return_ty);
943        }
944        if let Some(f) = self.api.gdscript_builtin(name) {
945            return resolve::layer_to_ty(self.api, f.ret);
946        }
947        // A builtin / class name used as a constructor: `Vector2(...)` / `Array(...)`.
948        // Normalize via `resolve_tyref` so `Array`/`Dictionary`/`Callable`/`Signal` land on
949        // their dedicated `Ty` variants rather than `Builtin(...)`.
950        if let Some(b) = self.api.builtin_by_name(name) {
951            return ty::resolve_tyref(self.api, &TyRef::Builtin(b));
952        }
953        // Otherwise unresolved — most likely a cross-file global / autoload / a method on a
954        // `class_name` base we can't see. Treat as the seam so `var x := foo()` never warns.
955        Ty::Unknown
956    }
957
958    fn func_return_ty(&self, annotation: Option<&str>) -> Ty {
959        annotation.map_or(Ty::Variant, |t| {
960            resolve::resolve_type_name(self.db, self.api, t)
961        })
962    }
963
964    /// Member access `receiver.name`. When `as_method`, resolve a method (and use its return
965    /// type); otherwise resolve a property/const/etc. Raises `UNSAFE_*` only on a statically
966    /// **known** receiver.
967    fn infer_field(
968        &mut self,
969        receiver: ExprId,
970        name: &str,
971        name_range: TextRange,
972        as_method: bool,
973    ) -> Ty {
974        let is_self = matches!(self.body.expr(receiver), Expr::SelfExpr);
975        let recv_ty = self.infer_expr(receiver, &Expectation::None);
976
977        // `self.member` consults this file's own members first (Playbook §3.2).
978        if is_self && let Some(item) = self.class.lookup(name) {
979            return self.own_member_ty(item, as_method);
980        }
981
982        match &recv_ty {
983            // Uninformative receivers are unchecked and **propagate the seam**: a member of an
984            // `Unknown` (cross-file) value is itself `Unknown` (never warns), a member of a
985            // `Variant` is `Variant`, of an `Error` is `Error`. Collapsing `Unknown` to
986            // `Variant` here would wrongly fire `INFERENCE_ON_VARIANT` on `var x := other.field`.
987            t if t.is_uninformative() => recv_ty.clone(),
988            Ty::Object(class) => {
989                if name == "new" {
990                    // `Class.new(...)` always constructs an instance of the class (some classes,
991                    // e.g. GDScript, also carry a modeled `new` member — the constructor wins).
992                    recv_ty.clone()
993                } else if let Some(m) = self.api.lookup_member(*class, name) {
994                    self.member_ref_ty(&m, as_method)
995                } else if let Some(t) = self.class_enum_value(*class, name) {
996                    // A statically-accessed enum value (`Control.PRESET_FULL_RECT`).
997                    t
998                } else {
999                    // Self with an Object base already checked own members above.
1000                    self.emit_unsafe(name, &recv_ty, name_range, as_method);
1001                    Ty::Variant
1002                }
1003            }
1004            Ty::Builtin(_) | Ty::Array(_) | Ty::Dict(..) | Ty::Callable | Ty::Signal(_) => {
1005                self.builtin_member_ty(&recv_ty, name, name_range, as_method)
1006            }
1007            // Enum value access (`MyEnum.VALUE`) is an `int`.
1008            Ty::Enum(_) => self.int_ty(),
1009            // A cross-file script reference: resolve the member against its (own) member table.
1010            Ty::ScriptRef(sref) => self.script_member_ty(*sref, name, as_method),
1011            _ => Ty::Variant,
1012        }
1013    }
1014
1015    /// A member of a cross-file script (`ScriptRef`): looked up in the script's own member table
1016    /// (M1). A member we don't model — e.g. one inherited from a base we don't resolve until M2 —
1017    /// yields the seam (`Unknown`), **never** an `UNSAFE_*` warning. `Class.new(...)` constructs
1018    /// an instance of the class.
1019    fn script_member_ty(&self, sref: ScriptRefId, name: &str, as_method: bool) -> Ty {
1020        if name == "new" {
1021            return Ty::ScriptRef(sref);
1022        }
1023        self.script_member_walk(sref, name, as_method, 0)
1024            .unwrap_or(Ty::Unknown)
1025    }
1026
1027    /// Walk a script class's `extends` chain for `name`: own members first, then a user base
1028    /// (another `ScriptRef`), then an engine base (the API table). Depth-bounded so a cyclic
1029    /// `extends` cannot loop. `None` = not found anywhere in the chain (the seam).
1030    fn script_member_walk(
1031        &self,
1032        sref: ScriptRefId,
1033        name: &str,
1034        as_method: bool,
1035        depth: u32,
1036    ) -> Option<Ty> {
1037        if depth > 32 {
1038            return None;
1039        }
1040        let file = self.db.file_text(FileId(sref.0))?;
1041        let sc = crate::queries::script_class(self.db, file);
1042        if let Some(m) = sc.member(name) {
1043            return Some(match m {
1044                crate::queries::MemberSig::Method(ret) => {
1045                    if as_method {
1046                        ret.clone()
1047                    } else {
1048                        Ty::Callable
1049                    }
1050                }
1051                crate::queries::MemberSig::Field(t) => t.clone(),
1052                crate::queries::MemberSig::Signal => Ty::Signal(None),
1053            });
1054        }
1055        // Not an own member — continue up the inheritance chain.
1056        match sc.base() {
1057            Ty::ScriptRef(base) => self.script_member_walk(*base, name, as_method, depth + 1),
1058            Ty::Object(class) => self
1059                .api
1060                .lookup_member(*class, name)
1061                .map(|m| self.member_ref_ty(&m, as_method)),
1062            _ => None,
1063        }
1064    }
1065
1066    /// Whether a value of type `sub` is statically a subtype of `sup` — composing user `ScriptRef`
1067    /// `extends` chains with the engine class table (M4, for `is`/`as` widen-only narrowing). A
1068    /// `ScriptRef` IS-A its native base (so `script_value is Node` holds), but Godot's asymmetry is
1069    /// honored: a native/script value is **not** a subtype of an *unrelated* user script.
1070    fn is_subtype(&self, sub: &Ty, sup: &Ty) -> bool {
1071        match (sub, sup) {
1072            (Ty::Object(a), Ty::Object(b)) => self.api.is_subclass(*a, *b),
1073            (Ty::ScriptRef(a), Ty::ScriptRef(b)) => self.script_is_subtype(*a, *b, 0),
1074            (Ty::ScriptRef(a), Ty::Object(b)) => self.script_extends_engine(*a, *b, 0),
1075            _ => false,
1076        }
1077    }
1078
1079    /// Whether script `sub` is `sup` or transitively extends it — walk the `extends` base chain by
1080    /// script identity (depth-bounded, like [`script_member_walk`](Self::script_member_walk)).
1081    fn script_is_subtype(&self, sub: ScriptRefId, sup: ScriptRefId, depth: u32) -> bool {
1082        if depth > 32 {
1083            return false;
1084        }
1085        if sub == sup {
1086            return true;
1087        }
1088        let Some(file) = self.db.file_text(FileId(sub.0)) else {
1089            return false;
1090        };
1091        match crate::queries::script_class(self.db, file).base() {
1092            Ty::ScriptRef(base) => self.script_is_subtype(*base, sup, depth + 1),
1093            _ => false,
1094        }
1095    }
1096
1097    /// Whether script `sub`'s `extends` chain reaches engine class `sup_native` at its native base.
1098    fn script_extends_engine(
1099        &self,
1100        sub: ScriptRefId,
1101        sup_native: gdscript_api::ClassId,
1102        depth: u32,
1103    ) -> bool {
1104        if depth > 32 {
1105            return false;
1106        }
1107        let Some(file) = self.db.file_text(FileId(sub.0)) else {
1108            return false;
1109        };
1110        match crate::queries::script_class(self.db, file).base() {
1111            Ty::ScriptRef(base) => self.script_extends_engine(*base, sup_native, depth + 1),
1112            Ty::Object(native) => self.api.is_subclass(*native, sup_native),
1113            _ => false,
1114        }
1115    }
1116
1117    fn emit_unsafe(&mut self, name: &str, recv: &Ty, range: TextRange, as_method: bool) {
1118        let recv_label = recv.label(self.api).unwrap_or_else(|| "?".to_owned());
1119        let (code, message) = if as_method {
1120            (
1121                UNSAFE_METHOD_ACCESS,
1122                format!(
1123                    "The method \"{name}()\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1124                ),
1125            )
1126        } else {
1127            (
1128                UNSAFE_PROPERTY_ACCESS,
1129                format!(
1130                    "The property \"{name}\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1131                ),
1132            )
1133        };
1134        self.emit(range, Severity::Warning, code, message);
1135    }
1136
1137    fn member_ref_ty(&self, m: &MemberRef, as_method: bool) -> Ty {
1138        match m {
1139            MemberRef::Method(sig) => {
1140                if as_method {
1141                    ty::resolve_tyref(self.api, &sig.return_ty)
1142                } else {
1143                    Ty::Callable
1144                }
1145            }
1146            MemberRef::Property(p) => p.enum_of.as_ref().map_or_else(
1147                || ty::resolve_tyref(self.api, &p.ty),
1148                |q| {
1149                    Ty::Enum(EnumRef {
1150                        qualified: SmolStr::new(q),
1151                        bitfield: false,
1152                    })
1153                },
1154            ),
1155            MemberRef::Const(c) => ty::resolve_tyref(self.api, &c.ty),
1156            MemberRef::Signal(_) => Ty::Signal(None),
1157            MemberRef::Enum(_) => Ty::Variant,
1158        }
1159    }
1160
1161    fn builtin_member_ty(
1162        &mut self,
1163        recv: &Ty,
1164        name: &str,
1165        range: TextRange,
1166        as_method: bool,
1167    ) -> Ty {
1168        let Some(bid) = self.builtin_id_of(recv) else {
1169            return Ty::Variant;
1170        };
1171        if as_method {
1172            return if let Some(sig) = self.api.builtin_method(bid, name) {
1173                ty::resolve_tyref(self.api, &sig.return_ty)
1174            } else {
1175                self.emit_unsafe(name, recv, range, true);
1176                Ty::Variant
1177            };
1178        }
1179        if let Some(member) = self.api.builtin_member(bid, name) {
1180            return ty::resolve_tyref(self.api, &member.ty);
1181        }
1182        // Static constants (`Vector2.ZERO`, `Color.WHITE`) and enum values (`Variant.Type.*`).
1183        let data = self.api.builtin(bid);
1184        if let Some(c) = data.constants.iter().find(|c| c.name == name) {
1185            return ty::resolve_tyref(self.api, &c.ty);
1186        }
1187        if data
1188            .enums
1189            .iter()
1190            .any(|e| e.values.iter().any(|v| v.name == name))
1191        {
1192            return self.int_ty();
1193        }
1194        if self.api.builtin_method(bid, name).is_some() {
1195            return Ty::Callable;
1196        }
1197        self.emit_unsafe(name, recv, range, false);
1198        Ty::Variant
1199    }
1200
1201    /// The type of a class enum **value** accessed statically (`Control.PRESET_FULL_RECT`):
1202    /// the engine exposes enum values as class members, so search every (inherited) enum's
1203    /// values. Returns `int` when found.
1204    fn class_enum_value(&self, class: gdscript_api::ClassId, name: &str) -> Option<Ty> {
1205        let mut cur = Some(class);
1206        while let Some(cid) = cur {
1207            let c = self.api.class(cid);
1208            if c.enums
1209                .iter()
1210                .any(|e| e.values.iter().any(|v| v.name == name))
1211            {
1212                return Some(self.int_ty());
1213            }
1214            cur = c.base;
1215        }
1216        None
1217    }
1218
1219    /// The builtin id backing a builtin / `Array` / `Dictionary` receiver.
1220    fn builtin_id_of(&self, ty: &Ty) -> Option<gdscript_api::BuiltinId> {
1221        match ty {
1222            Ty::Builtin(b) => Some(*b),
1223            Ty::Array(_) => self.api.builtin_by_name("Array"),
1224            Ty::Dict(..) => self.api.builtin_by_name("Dictionary"),
1225            Ty::Callable => self.api.builtin_by_name("Callable"),
1226            Ty::Signal(_) => self.api.builtin_by_name("Signal"),
1227            _ => None,
1228        }
1229    }
1230
1231    /// The element type of an indexing expression (Playbook §2 switch).
1232    fn index_ty(&self, base: &Ty) -> Ty {
1233        match base {
1234            Ty::Array(elem) => (**elem).clone(),
1235            Ty::Builtin(b) => self
1236                .api
1237                .builtin(*b)
1238                .indexing_return
1239                .as_ref()
1240                .map_or(Ty::Variant, |r| ty::resolve_tyref(self.api, r)),
1241            // Indexing through the seam stays on the seam (never warns).
1242            Ty::Unknown => Ty::Unknown,
1243            Ty::Error => Ty::Error,
1244            _ => Ty::Variant,
1245        }
1246    }
1247
1248    /// The loop variable's type for `for v in iter:` (Playbook §2 switch).
1249    fn loop_var_ty(&self, iter: &Ty) -> Ty {
1250        match iter {
1251            Ty::Array(elem) => (**elem).clone(),
1252            Ty::Builtin(b) => {
1253                let data = self.api.builtin(*b);
1254                if data.name == "int" {
1255                    // `for i in 5` / `for i in range(...)` → int.
1256                    self.int_ty()
1257                } else if let Some(r) = &data.indexing_return {
1258                    // `for c in "abc"` → String; `for s in packed_string_array` → String; …
1259                    ty::resolve_tyref(self.api, r)
1260                } else {
1261                    Ty::Variant
1262                }
1263            }
1264            // Iterating a seam value keeps the loop var on the seam (never warns).
1265            Ty::Unknown => Ty::Unknown,
1266            Ty::Error => Ty::Error,
1267            _ => Ty::Variant,
1268        }
1269    }
1270
1271    fn infer_lambda(&mut self, params: &[ParamBinding], body: &[body::StmtId]) {
1272        // Lambda params shadow within the body; restore the outer locals afterward. A `return`
1273        // inside the lambda is the *lambda's* return, not the enclosing function's — so disable
1274        // return checking (set the expected return to `Variant`) while walking the body.
1275        let saved_locals = self.locals.clone();
1276        let saved_ret = std::mem::replace(&mut self.return_ty, Ty::Variant);
1277        for p in params {
1278            let ty = self.param_ty(p);
1279            self.bindings.push(Binding {
1280                name_range: p.name_range,
1281                ty: ty.clone(),
1282                init: None,
1283                annotated: p.type_ref.is_some(),
1284                inferred_colon_eq: false,
1285                kind: BindingKind::Param,
1286            });
1287            self.locals.insert(p.name.clone(), ty);
1288        }
1289        self.infer_block(body);
1290        self.return_ty = saved_ret;
1291        self.locals = saved_locals;
1292    }
1293
1294    fn param_ty(&mut self, p: &ParamBinding) -> Ty {
1295        if let Some(ptr) = p.type_ref {
1296            return self.resolve_ptr_ty(ptr);
1297        }
1298        // An unannotated param infers from its default, else `Variant`.
1299        p.default
1300            .map_or(Ty::Variant, |e| self.infer_expr(e, &Expectation::None))
1301    }
1302
1303    // ---- name resolution (local → class member → inherited → global) ----
1304
1305    fn resolve_name(&mut self, id: ExprId, name: &str) -> Ty {
1306        // Flow narrowing wins over the binding's declared type.
1307        if let Some(key) = self.narrow_key(id)
1308            && let Some(t) = self.narrowing.get(&key)
1309        {
1310            return t.clone();
1311        }
1312        if let Some(t) = self.locals.get(name) {
1313            return t.clone();
1314        }
1315        if let Some(item) = self.class.lookup(name) {
1316            return self.own_member_ty(item, false);
1317        }
1318        // Inherited members: an engine `Object` base via the API table, or a user `ScriptRef`
1319        // base via the script member walk (M2 — so a class extending another class_name sees its
1320        // inherited members).
1321        match self.class.base.clone() {
1322            Ty::Object(base) => {
1323                if let Some(m) = self.api.lookup_member(base, name) {
1324                    return self.member_ref_ty(&m, false);
1325                }
1326            }
1327            Ty::ScriptRef(base) => {
1328                if let Some(t) = self.script_member_walk(base, name, false, 0) {
1329                    return t;
1330                }
1331            }
1332            _ => {}
1333        }
1334        if let Some(g) = resolve::resolve_global(self.api, name) {
1335            return global_ty(&g);
1336        }
1337        // A project-global `class_name` used as a value — the class itself, for static access
1338        // (`V.fc()`) or as a constructor (`Player.new()`). Resolves to a `ScriptRef` via the
1339        // registry. Precedence (Godot `reduce_identifier`): `class_name` global ≫ autoload
1340        // singleton. So try `class_name` first, then a `*`-autoload, then the seam.
1341        let by_class = resolve::resolve_external(
1342            self.db,
1343            &resolve::ExternalRef::ClassName(SmolStr::new(name)),
1344        );
1345        if !by_class.is_unknown() {
1346            return by_class;
1347        }
1348        resolve::resolve_external(self.db, &resolve::ExternalRef::Autoload(SmolStr::new(name)))
1349    }
1350
1351    fn own_member_ty(&self, item: ClassItem, as_method: bool) -> Ty {
1352        match item {
1353            ClassItem::EnumVariant => self.int_ty(),
1354            ClassItem::Member(_) => match self.class.member(item) {
1355                Some(Member::Var(v)) => self.field_ty(&v.name, v.ptr),
1356                Some(Member::Const(c)) => self.field_ty(&c.name, c.ptr),
1357                Some(Member::Func(f)) => {
1358                    if as_method {
1359                        self.func_return_ty(f.return_type.as_deref())
1360                    } else {
1361                        Ty::Callable
1362                    }
1363                }
1364                Some(Member::Signal(_)) => Ty::Signal(None),
1365                Some(Member::Class(_)) => Ty::Unknown,
1366                Some(Member::Enum(_)) | None => Ty::Variant,
1367            },
1368        }
1369    }
1370
1371    /// The type of an own field (`var`/`const`): the type seeded by the field pre-pass (which
1372    /// captures the inferred type of `var n := 0`), falling back to the written annotation.
1373    fn field_ty(&self, name: &str, ptr: AstPtr) -> Ty {
1374        if let Some(t) = self.class.member_types.get(name) {
1375            return t.clone();
1376        }
1377        self.resolve_decl_annotation(ptr)
1378    }
1379
1380    /// Resolve a declaration's annotation (recovering its `TypeRef` node), else `Variant`.
1381    fn resolve_decl_annotation(&self, ptr: AstPtr) -> Ty {
1382        let Some(node) = ptr.to_node(self.root) else {
1383            return Ty::Variant;
1384        };
1385        cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
1386            .map_or(Ty::Variant, |t| {
1387                resolve::resolve_type_ref(self.db, self.api, &t)
1388            })
1389    }
1390
1391    // ---- narrowing ----
1392
1393    /// Apply the narrowing implied by an `if`/`elif` condition to the current (cloned) branch.
1394    ///
1395    /// `is`-narrowing is a deliberate divergence from upstream Godot (whose `is` does **not**
1396    /// flow-narrow); we add it as a UX improvement but keep it **widen-only** so it never produces
1397    /// a type Godot's checker would reject: narrow only when the tested type is a strict downcast
1398    /// of the operand's current type, or the operand is uninformative. If the operand is already a
1399    /// subtype of the test (`d: Derived; if d is Base`), keep it — do not un-narrow. The
1400    /// `is_uninformative` guard also stays: never narrow to a type we couldn't resolve.
1401    fn apply_narrowing(&mut self, cond: ExprId) {
1402        let Expr::Is {
1403            operand,
1404            ty: Some(ptr),
1405            negated: false,
1406        } = self.body.expr(cond).clone()
1407        else {
1408            return;
1409        };
1410        let Some(key) = self.narrow_key(operand) else {
1411            return;
1412        };
1413        let narrowed = self.resolve_ptr_ty(ptr);
1414        if narrowed.is_uninformative() {
1415            return;
1416        }
1417        let cur = self.expr_ty.get(&operand).cloned().unwrap_or(Ty::Variant);
1418        if cur.is_uninformative() || self.is_subtype(&narrowed, &cur) {
1419            self.narrowing.insert(key, narrowed);
1420        }
1421    }
1422
1423    /// A dotted access-path key for narrowing (`x`, `self.field`, `a.b.c`), or `None` for a
1424    /// non-path expression.
1425    fn narrow_key(&self, id: ExprId) -> Option<String> {
1426        match self.body.expr(id) {
1427            Expr::Name(n) => Some(n.to_string()),
1428            Expr::SelfExpr => Some("self".to_owned()),
1429            Expr::Paren(inner) => self.narrow_key(*inner),
1430            Expr::Field { receiver, name, .. } => {
1431                Some(format!("{}.{name}", self.narrow_key(*receiver)?))
1432            }
1433            _ => None,
1434        }
1435    }
1436
1437    fn resolve_ptr_ty(&self, ptr: AstPtr) -> Ty {
1438        ptr.to_node(self.root).map_or(Ty::Variant, |n| {
1439            resolve::resolve_type_ref(self.db, self.api, &n)
1440        })
1441    }
1442
1443    // ---- helpers ----
1444
1445    /// The join (least upper bound) of two branch types — conservative: equal types collapse,
1446    /// a subtype widens to its supertype, else `Variant`.
1447    ///
1448    /// The three uninformative markers do NOT collapse to `Variant` — that would defeat the
1449    /// seam. They propagate by priority: `Error` (already diagnosed) → `Unknown` (the cross-file
1450    /// seam — must never warn or cascade) → `Variant` (the gradual top). So
1451    /// `x if c else <unknown>` stays `Unknown`, and `var y := (x if c else unknown)` does not
1452    /// fire a false `INFERENCE_ON_VARIANT`.
1453    fn join(&self, a: &Ty, b: &Ty) -> Ty {
1454        if a == b {
1455            return a.clone();
1456        }
1457        if a.is_error() || b.is_error() {
1458            return Ty::Error;
1459        }
1460        if a.is_unknown() || b.is_unknown() {
1461            return Ty::Unknown;
1462        }
1463        if a.is_variant() || b.is_variant() {
1464            return Ty::Variant;
1465        }
1466        if ty::is_assignable(self.api, a, b) == Assign::Ok {
1467            return b.clone();
1468        }
1469        if ty::is_assignable(self.api, b, a) == Assign::Ok {
1470            return a.clone();
1471        }
1472        Ty::Variant
1473    }
1474
1475    /// Run a closure within a branch-scoped narrowing frame (clone on enter, restore on exit).
1476    fn in_branch<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
1477        let saved = self.narrowing.clone();
1478        let r = f(self);
1479        self.narrowing = saved;
1480        r
1481    }
1482}
1483
1484/// Map a resolved global definition to the type of a bare reference to it.
1485fn global_ty(g: &GlobalDef) -> Ty {
1486    match g {
1487        GlobalDef::Const(t) => t.clone(),
1488        GlobalDef::Singleton(c) | GlobalDef::ClassType(c) => Ty::Object(*c),
1489        GlobalDef::BuiltinType(b) => Ty::Builtin(*b),
1490        // A bare function referenced as a value is a `Callable`; an enum namespace is opaque.
1491        GlobalDef::Builtin | GlobalDef::Utility => Ty::Callable,
1492        GlobalDef::GlobalEnum => Ty::Variant,
1493    }
1494}
1495
1496fn inference_on_variant_msg(kind: &str) -> String {
1497    format!(
1498        "The {kind} type is being inferred from a Variant value, so it will be typed as Variant."
1499    )
1500}
1501
1502/// The `extension_api.json` operator spelling for a binary operator.
1503fn op_symbol(op: BinOp) -> Option<&'static str> {
1504    Some(match op {
1505        BinOp::Add => "+",
1506        BinOp::Sub => "-",
1507        BinOp::Mul => "*",
1508        BinOp::Div => "/",
1509        BinOp::Mod => "%",
1510        BinOp::Pow => "**",
1511        BinOp::BitAnd => "&",
1512        BinOp::BitOr => "|",
1513        BinOp::BitXor => "^",
1514        BinOp::Shl => "<<",
1515        BinOp::Shr => ">>",
1516        _ => return None,
1517    })
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522    use super::*;
1523    use crate::item_tree::item_tree;
1524    use gdscript_syntax::{SyntaxKind, parse};
1525
1526    struct Harness {
1527        result: InferenceResult,
1528        body: Body,
1529    }
1530
1531    /// Infer the (first) function in `src` against a fresh class scope.
1532    fn infer_first_func(src: &str) -> Harness {
1533        let api = gdscript_api::bundled();
1534        let db = gdscript_db::RootDatabase::default();
1535        let root = parse(src).syntax_node();
1536        let tree = item_tree(&root);
1537        let class = ClassScope::new(&db, api, &tree);
1538        let func = gdscript_syntax::ast::descendants(&root)
1539            .into_iter()
1540            .find(|n| n.kind() == SyntaxKind::FuncDecl)
1541            .expect("a function");
1542        let body = body::body_of_func(&func);
1543        let return_ty = cst::first_child(&func, |k| k == SyntaxKind::TypeRef)
1544            .map_or(Ty::Variant, |t| resolve::resolve_type_ref(&db, api, &t));
1545        let result = infer(&db, api, &root, &class, &body, return_ty);
1546        Harness { result, body }
1547    }
1548
1549    fn codes(h: &Harness) -> Vec<&str> {
1550        h.result
1551            .diagnostics
1552            .iter()
1553            .map(|d| d.code.as_str())
1554            .collect()
1555    }
1556
1557    #[test]
1558    fn integer_division_warns() {
1559        let h = infer_first_func("func f():\n\tvar x = 5 / 2\n");
1560        assert!(codes(&h).contains(&INTEGER_DIVISION));
1561    }
1562
1563    #[test]
1564    fn float_div_does_not_warn() {
1565        let h = infer_first_func("func f():\n\tvar x = 5.0 / 2\n");
1566        assert!(!codes(&h).contains(&INTEGER_DIVISION));
1567    }
1568
1569    #[test]
1570    fn type_mismatch_on_hard_annotation() {
1571        let h = infer_first_func("func f():\n\tvar s: String = 5\n");
1572        assert!(codes(&h).contains(&TYPE_MISMATCH));
1573    }
1574
1575    #[test]
1576    fn narrowing_conversion_float_to_int() {
1577        let h = infer_first_func("func f():\n\tvar n: int = 1.5\n");
1578        assert!(codes(&h).contains(&NARROWING_CONVERSION));
1579    }
1580
1581    #[test]
1582    fn int_to_float_is_silent() {
1583        let h = infer_first_func("func f():\n\tvar x: float = 3\n");
1584        assert!(
1585            h.result.diagnostics.is_empty(),
1586            "{:?}",
1587            h.result.diagnostics
1588        );
1589    }
1590
1591    #[test]
1592    fn member_access_resolves_engine_property() {
1593        // In a Node script, bare `get_node(...)` resolves via the inherited base to Object(Node);
1594        // `get_parent()` is a real Node method → no UNSAFE.
1595        let h = infer_first_func(
1596            "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.get_parent()\n",
1597        );
1598        assert!(
1599            codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
1600            "{:?}",
1601            h.result.diagnostics
1602        );
1603    }
1604
1605    #[test]
1606    fn unsafe_method_on_known_type() {
1607        let h = infer_first_func(
1608            "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.totally_bogus_method()\n",
1609        );
1610        assert!(
1611            codes(&h).contains(&UNSAFE_METHOD_ACCESS),
1612            "{:?}",
1613            h.result.diagnostics
1614        );
1615    }
1616
1617    #[test]
1618    fn is_narrowing_suppresses_unsafe() {
1619        // Without narrowing, `x.free()` on an untyped param would be unchecked anyway; with
1620        // `is Node` it is checked against Node and `free` IS a Node method → no UNSAFE.
1621        let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.queue_free()\n");
1622        assert!(
1623            codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
1624            "{:?}",
1625            h.result.diagnostics
1626        );
1627    }
1628
1629    #[test]
1630    fn is_narrowing_flags_real_missing_member() {
1631        // After `is Node`, x is Node; `.bogus()` is genuinely missing → UNSAFE.
1632        let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.bogus_method()\n");
1633        assert!(codes(&h).contains(&UNSAFE_METHOD_ACCESS));
1634    }
1635
1636    #[test]
1637    fn variant_receiver_never_unsafe() {
1638        // Untyped param → Variant receiver → unchecked, no diagnostic.
1639        let h = infer_first_func("func f(x):\n\tx.anything_at_all()\n");
1640        assert!(
1641            h.result.diagnostics.is_empty(),
1642            "{:?}",
1643            h.result.diagnostics
1644        );
1645    }
1646
1647    #[test]
1648    fn inference_on_variant() {
1649        // `:=` from an untyped (Variant) param.
1650        let h = infer_first_func("func f(x):\n\tvar y := x\n");
1651        assert!(codes(&h).contains(&INFERENCE_ON_VARIANT));
1652    }
1653
1654    #[test]
1655    fn lambda_var_is_callable_not_variant() {
1656        let h = infer_first_func("func f():\n\tvar cb := func():\n\t\tpass\n");
1657        assert!(
1658            !codes(&h).contains(&INFERENCE_ON_VARIANT),
1659            "{:?}",
1660            h.result.diagnostics
1661        );
1662    }
1663
1664    #[test]
1665    fn multiline_lambda_then_paren_line_no_false_warning() {
1666        // A multi-line lambda bound to a var, followed by a statement that begins with `(`.
1667        // The parser now ends the lambda at its dedent (the `(` line is its own statement), so
1668        // there is no spurious call-on-lambda and no false `INFERENCE_ON_VARIANT`.
1669        let src = "func f(state, i, loop):\n\tvar cb := func():\n\t\tif i >= state.size():\n\t\t\treturn\n\t(loop as SceneTree).process_frame.connect(cb, CONNECT_ONE_SHOT)\n";
1670        let h = infer_first_func(src);
1671        assert!(
1672            !codes(&h).contains(&INFERENCE_ON_VARIANT),
1673            "{:?}",
1674            h.result.diagnostics
1675        );
1676    }
1677
1678    #[test]
1679    fn calling_a_callable_value_is_seam_not_variant() {
1680        // Invoking an arbitrary expression (here a parenthesized `Callable` value) reaches the
1681        // seam arm of `infer_call`: the return type isn't tracked, so the result is Unknown,
1682        // not `Variant`, and the inferred-on-Variant warning never fires.
1683        let src = "func f(cb: Callable):\n\tvar x := (cb)()\n\treturn x\n";
1684        let h = infer_first_func(src);
1685        assert!(
1686            !codes(&h).contains(&INFERENCE_ON_VARIANT),
1687            "{:?}",
1688            h.result.diagnostics
1689        );
1690    }
1691
1692    #[test]
1693    fn ternary_with_seam_branch_does_not_collapse_to_variant() {
1694        // A ternary whose else-branch is the seam (`await` is untracked → Unknown) must `join`
1695        // to Unknown, NOT Variant — otherwise `var x := …` fires a false INFERENCE_ON_VARIANT.
1696        // (Regression: `join` used to absorb any uninformative branch to Variant.)
1697        let src =
1698            "func f(c: bool):\n\tvar x := 5 if c else await get_tree().process_frame\n\treturn x\n";
1699        let h = infer_first_func(src);
1700        assert!(
1701            !codes(&h).contains(&INFERENCE_ON_VARIANT),
1702            "seam branch should keep the ternary on the seam: {:?}",
1703            h.result.diagnostics
1704        );
1705    }
1706
1707    #[test]
1708    fn for_var_over_packed_string_array_is_string() {
1709        // `for s in "a,b".split(",")` iterates a PackedStringArray → String, so `s.to_int()`
1710        // resolves and `var x := s` does not warn.
1711        let h = infer_first_func("func f():\n\tfor s in \"a,b\".split(\",\"):\n\t\tvar x := s\n");
1712        assert!(
1713            !codes(&h).contains(&INFERENCE_ON_VARIANT),
1714            "{:?}",
1715            h.result.diagnostics
1716        );
1717    }
1718
1719    #[test]
1720    fn class_new_is_object_not_variant() {
1721        let h = infer_first_func("func f():\n\tvar s := GDScript.new()\n");
1722        assert!(
1723            !codes(&h).contains(&INFERENCE_ON_VARIANT),
1724            "{:?}",
1725            h.result.diagnostics
1726        );
1727    }
1728
1729    #[test]
1730    fn unknown_seam_never_warns() {
1731        // `preload(...)` is Unknown; `:=` from it does NOT warn, and member access is unchecked.
1732        let h = infer_first_func("func f():\n\tvar s := preload(\"res://x.gd\")\n\ts.whatever()\n");
1733        assert!(
1734            h.result.diagnostics.is_empty(),
1735            "{:?}",
1736            h.result.diagnostics
1737        );
1738    }
1739
1740    #[test]
1741    fn expr_types_are_memoized_for_hover() {
1742        let h = infer_first_func("func f():\n\tvar n := 42\n");
1743        // The `42` literal expr should be typed int.
1744        let has_int = h
1745            .result
1746            .expr_ty
1747            .values()
1748            .any(|t| matches!(t, Ty::Builtin(_)));
1749        assert!(has_int);
1750        // sanity: the body lowered at least one expr
1751        assert!(!h.body.exprs.is_empty());
1752    }
1753}