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/// An argument whose static type needs an unsafe implicit cast (`Variant` / a downcast) into the
43/// resolved parameter type — Godot's per-argument value-prop warning.
44pub const UNSAFE_CALL_ARGUMENT: &str = "UNSAFE_CALL_ARGUMENT";
45/// A `$Path`/`%Unique`/`get_node("…")` whose literal path is genuinely absent in the owning scene
46/// (only raised when the script attaches to exactly one scene — never on an `..`/absolute path or a
47/// path that descends into an instanced sub-scene we don't see).
48pub const INVALID_NODE_PATH: &str = "INVALID_NODE_PATH";
49/// A declared `class_name` that shadows another global identifier — a duplicate user `class_name`,
50/// an engine/native class, a builtin/utility, a global enum/const, or a `*`-autoload singleton.
51/// Godot's `gdscript_analyzer.cpp` raises this (as an error) so the global namespace stays unique.
52pub const SHADOWED_GLOBAL_IDENTIFIER: &str = "SHADOWED_GLOBAL_IDENTIFIER";
53/// A genuine `extends` cycle: a file's base chain transitively returns to itself (`A extends B`,
54/// `B extends A`). Illegal in Godot (`gdscript_analyzer.cpp` raises it). Only the `extends`
55/// inheritance chain cycles — a `preload`/`load` cycle is legal at runtime and is NOT reported.
56pub const CYCLIC_INHERITANCE: &str = "CYCLIC_INHERITANCE";
57
58/// What kind of binding a [`Binding`] describes.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum BindingKind {
61    /// A local `var` / `const`.
62    Var,
63    /// A function / lambda parameter.
64    Param,
65    /// A `for` loop variable.
66    ForVar,
67    /// A `var x` capture in a `match` pattern (typed `Variant`; arm-scoped).
68    MatchBind,
69}
70
71/// A typed local binding — the unit hover + inlay hints read for `var`/param/`for` names.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Binding {
74    /// The name token's range.
75    pub name_range: TextRange,
76    /// The binding's resolved type. For an untyped `var x = e` this is the gradual `Variant`;
77    /// the precise initializer type (for an "add type annotation" action) is [`Binding::init`].
78    pub ty: Ty,
79    /// The initializer expression, when the binding has one (a `var`/`const` with `= e`).
80    pub init: Option<ExprId>,
81    /// Whether the source carried an explicit `: T` annotation.
82    pub annotated: bool,
83    /// Whether the source used `:=` (inferred-but-hard).
84    pub inferred_colon_eq: bool,
85    /// What kind of binding this is.
86    pub kind: BindingKind,
87}
88
89/// The result of inferring one body.
90#[derive(Debug, Clone, Default, PartialEq, Eq)]
91pub struct InferenceResult {
92    /// Every expression's inferred type (feeds hover + inlay).
93    pub expr_ty: FxHashMap<ExprId, Ty>,
94    /// The local bindings introduced by the body (params, `var`/`const`, `for` vars).
95    pub bindings: Vec<Binding>,
96    /// The §5 type diagnostics raised.
97    pub diagnostics: Vec<Diagnostic>,
98}
99
100impl InferenceResult {
101    /// The inferred type of an expression, if it was visited.
102    #[must_use]
103    pub fn type_of(&self, id: ExprId) -> Option<&Ty> {
104        self.expr_ty.get(&id)
105    }
106
107    /// The binding whose name token contains `offset`, if any.
108    #[must_use]
109    pub fn binding_at(&self, offset: u32) -> Option<&Binding> {
110        self.bindings
111            .iter()
112            .find(|b| b.name_range.start <= offset && offset < b.name_range.end)
113    }
114}
115
116/// Infer a lowered `body` (its `tail` initializer expression and/or its statement block).
117/// `return_ty` is the function's declared return type (`Variant` if none / for an
118/// initializer body).
119#[must_use]
120pub fn infer(
121    db: &dyn Db,
122    api: &EngineApi,
123    root: &GdNode,
124    class: &ClassScope,
125    body: &Body,
126    return_ty: Ty,
127) -> InferenceResult {
128    let self_ty = class.self_ty.clone();
129    let mut cx = Cx {
130        db,
131        api,
132        root,
133        body,
134        class,
135        self_ty,
136        return_ty,
137        expr_ty: FxHashMap::default(),
138        bindings: Vec::new(),
139        diagnostics: Vec::new(),
140        locals: FxHashMap::default(),
141        narrowing: FxHashMap::default(),
142    };
143    // Parameters bind first (their defaults can reference earlier params).
144    let params = body.params.clone();
145    for p in &params {
146        let ty = cx.param_ty(p);
147        cx.bindings.push(Binding {
148            name_range: p.name_range,
149            ty: ty.clone(),
150            init: None,
151            annotated: p.type_ref.is_some(),
152            inferred_colon_eq: false,
153            kind: BindingKind::Param,
154        });
155        cx.locals.insert(p.name.clone(), ty);
156    }
157    if let Some(tail) = body.tail {
158        cx.infer_expr(tail, &Expectation::None);
159    }
160    let block = body.block.clone();
161    cx.infer_block(&block);
162    InferenceResult {
163        expr_ty: cx.expr_ty,
164        bindings: cx.bindings,
165        diagnostics: cx.diagnostics,
166    }
167}
168
169/// Convenience: recover a function node from its [`AstPtr`], lower its body, resolve its
170/// declared return type, and infer it.
171#[must_use]
172pub fn infer_func(
173    db: &dyn Db,
174    api: &EngineApi,
175    root: &GdNode,
176    class: &ClassScope,
177    ptr: AstPtr,
178) -> InferenceResult {
179    let Some(node) = ptr.to_node(root) else {
180        return InferenceResult::default();
181    };
182    let body = body::body_of_func(&node);
183    // The return-type annotation is the FuncDecl's direct `TypeRef` child (params' type refs
184    // are nested inside the ParamList, so they are not direct children).
185    let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
186        .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
187    infer(db, api, root, class, &body, return_ty)
188}
189
190/// One inferred unit of a file: a function body or a class field's initializer, with its
191/// lowered [`Body`] and [`InferenceResult`] (kept so position-based features — hover, inlay,
192/// member completion — can map a cursor back through the source map).
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct Unit {
195    /// The source range this unit covers (the function decl or the field decl).
196    pub range: TextRange,
197    /// The lowered body.
198    pub body: Body,
199    /// The inference result.
200    pub result: InferenceResult,
201}
202
203/// The full single-file inference: the item tree, every inferred unit, and the merged §5
204/// diagnostics. The whole-file entry point the IDE layer consumes.
205#[derive(Debug, Clone, PartialEq, Eq, Default)]
206pub struct FileInference {
207    /// The lowered item tree.
208    pub tree: Arc<ItemTree>,
209    /// The inferred function/field units.
210    pub units: Vec<Unit>,
211    /// All type diagnostics, merged across units.
212    pub diagnostics: Vec<Diagnostic>,
213}
214
215impl FileInference {
216    /// The innermost unit whose range contains `offset`.
217    #[must_use]
218    pub fn unit_at(&self, offset: u32) -> Option<&Unit> {
219        self.units
220            .iter()
221            .filter(|u| u.range.start <= offset && offset < u.range.end)
222            .min_by_key(|u| u.range.end - u.range.start)
223    }
224}
225
226/// Infer an entire file: lower its item tree, then infer every function body and every
227/// class-field initializer against a shared [`ClassScope`]. The single entry point for the
228/// IDE features (Playbook §6 — a pure `(api, parsed file) -> result` function).
229#[must_use]
230pub fn analyze_file(db: &dyn Db, api: &EngineApi, root: &GdNode, file_id: FileId) -> FileInference {
231    let tree = item_tree(root);
232    let mut units = Vec::new();
233    let mut diagnostics = Vec::new();
234    let mut member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
235    // `self` is the script's OWN class (a self-`ScriptRef`), not just its engine base — so member
236    // access on an aliased `self` resolves the file's own members (see `ClassScope::self_ty`).
237    let self_ref = Ty::ScriptRef(ScriptRefId(file_id.0));
238    // The file's own `res://` path, for anchoring relative `preload`/`extends` to its directory.
239    let res_path = db.file_text(file_id).and_then(|ft| ft.res_path(db));
240
241    // A declared `class_name` that collides with another global identifier (W2). Mirrors Godot's
242    // `gdscript_analyzer.cpp` uniqueness check over the global namespace, projected through the
243    // cross-file firewall (`class_name_collisions`) and the offset-free global resolvers — so it
244    // fires only when genuinely shadowing, never on the seam. Emitted once, at the decl's NAME.
245    if let Some(name) = tree.class_name.clone() {
246        let collides = collisions_contains(db, &name)
247            || resolve::resolve_global(api, &name).is_some()
248            || is_autoload_singleton(db, &name);
249        if collides && let Some(range) = class_name_decl_range(root) {
250            diagnostics.push(Diagnostic {
251                range,
252                severity: Severity::Warning,
253                code: SHADOWED_GLOBAL_IDENTIFIER.to_owned(),
254                message: format!(
255                    "The global class \"{name}\" hides a built-in/native/global/autoload."
256                ),
257                source: DiagnosticSource::Type,
258                fixes: Vec::new(),
259            });
260        }
261    }
262
263    // A genuine `extends` cycle (D7): walk THIS file's base chain by `FileId`; if it returns to the
264    // start, the inheritance is cyclic (illegal in Godot). Reported once, at the file's own `extends`
265    // decl range. Only `extends` cycles are walked here (member lookup is the only thing that loops);
266    // `preload`/`load` cycles are legal at runtime and never reach this resolver. We start by stepping
267    // ONTO the user base — if the very first base is the start file (`extends "res://self.gd"`, or two
268    // files A↔B), the revisit-of-start check fires; a deep but ACYCLIC chain bottoms out at an engine
269    // `Object`/`Unknown` and never revisits, so it does not false-fire.
270    if extends_chain_is_cyclic(db, file_id)
271        && let Some(range) = extends_decl_range(root)
272    {
273        diagnostics.push(Diagnostic {
274            range,
275            severity: Severity::Warning,
276            code: CYCLIC_INHERITANCE.to_owned(),
277            message: "Cyclic class hierarchy: this class's `extends` chain returns to itself."
278                .to_owned(),
279            source: DiagnosticSource::Type,
280            fixes: Vec::new(),
281        });
282    }
283
284    // Pass 1 — class fields. Inferring each `var`/`const` seeds `member_types` so the function
285    // pass sees the *inferred* field type (`var n := 0` → `int`), not just the annotation.
286    //
287    // A field initializer may reference an *earlier* field (`var a := 1` then `var b := a + 1`),
288    // so a single shallow round sees the referent as `Variant`/seam. We run a BOUNDED fixpoint:
289    // each round re-infers every field against the prior round's `member_types`, until the map
290    // stops changing or we hit the round cap. Cheap (fields are few, types settle in a round or
291    // two) and deterministic. Only the final round's units/diagnostics are kept — earlier rounds
292    // are throwaway probes feeding the seed.
293    {
294        // Bound the iteration: a linear `a -> b -> c -> …` chain settles in O(n) rounds, but a
295        // small constant is enough in practice (the corpus settles in ≤2) and guarantees
296        // termination even if a type oscillated.
297        const MAX_ROUNDS: usize = 4;
298        let mut final_units: Vec<Unit> = Vec::new();
299        let mut final_diagnostics: Vec<Diagnostic> = Vec::new();
300        for _ in 0..MAX_ROUNDS {
301            let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
302            class.self_ty = self_ref.clone();
303            class.member_types.clone_from(&member_types);
304            let mut next_member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
305            final_units = Vec::new();
306            final_diagnostics = Vec::new();
307            for m in &tree.members {
308                let (ptr, range) = match m {
309                    Member::Var(v) => (v.ptr, v.range),
310                    Member::Const(c) => (c.ptr, c.range),
311                    _ => continue,
312                };
313                if let Some(unit) = unit_from_decl(db, api, root, &class, ptr, range) {
314                    if let (Some(name), Some(b)) = (m.name(), unit.result.bindings.first()) {
315                        next_member_types.insert(SmolStr::new(name), b.ty.clone());
316                    }
317                    final_diagnostics.extend(unit.result.diagnostics.iter().cloned());
318                    final_units.push(unit);
319                }
320            }
321            if next_member_types == member_types {
322                break;
323            }
324            member_types = next_member_types;
325        }
326        diagnostics.extend(final_diagnostics);
327        units.extend(final_units);
328    }
329
330    // Pass 2 — functions, against a scope carrying the seeded field types.
331    {
332        let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
333        class.member_types = member_types;
334        class.self_ty = self_ref.clone();
335        for m in &tree.members {
336            let Member::Func(f) = m else { continue };
337            let Some(node) = f.ptr.to_node(root) else {
338                continue;
339            };
340            let body = body::body_of_func(&node);
341            let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
342                .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
343            let result = infer(db, api, root, &class, &body, return_ty);
344            diagnostics.extend(result.diagnostics.iter().cloned());
345            units.push(Unit {
346                range: f.range,
347                body,
348                result,
349            });
350        }
351    }
352
353    FileInference {
354        tree,
355        units,
356        diagnostics,
357    }
358}
359
360/// Whether `name` is declared as a `class_name` by more than one file in the project (W2). Reads
361/// the cross-file `class_name_collisions` firewall; `false` (no warning) when no source root is set
362/// — single-file analysis cannot observe a duplicate.
363fn collisions_contains(db: &dyn Db, name: &SmolStr) -> bool {
364    db.source_root()
365        .is_some_and(|root| crate::queries::class_name_collisions(db, root).contains(name))
366}
367
368/// Whether `name` is a `*`-flagged autoload singleton (a bare global). `false` when no
369/// `project.godot` is loaded — the seam, no warning.
370fn is_autoload_singleton(db: &dyn Db, name: &str) -> bool {
371    db.project_config().is_some_and(|config| {
372        crate::queries::autoload_registry(db, config)
373            .resolve_path(name)
374            .is_some()
375    })
376}
377
378/// The NAME range of the file's `class_name` declaration, trimmed to the bare identifier (the
379/// `Name` CST node absorbs leading inter-token trivia). `None` if the file declares no `class_name`
380/// or the decl has no name token. Mirrors `item_tree::trimmed_name_range` / navigation's
381/// `class_decl_target` (which lives in the IDE crate, hence this local CST scan).
382fn class_name_decl_range(root: &GdNode) -> Option<TextRange> {
383    use gdscript_syntax::SyntaxKind;
384    let decl = gdscript_syntax::ast::descendants(root)
385        .into_iter()
386        .find(|n| n.kind() == SyntaxKind::ClassNameDecl)?;
387    let name_node = decl.children().find(|c| c.kind() == SyntaxKind::Name)?;
388    let r = cst::text_range_of(name_node);
389    let text = name_node.text().to_string();
390    let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
391    let len = u32::try_from(text.trim().len()).unwrap_or(0);
392    Some(TextRange::new(r.start + lead, r.start + lead + len))
393}
394
395/// The byte range of the file's top-level `extends` declaration — the anchor for `CYCLIC_INHERITANCE`.
396/// Two surface forms: a standalone `extends Target` (an [`ExtendsClause`] child of the `SourceFile`),
397/// or the inline `class_name Name extends Target` (the `extends` keyword + target inside the
398/// [`ClassNameDecl`]). Scans only the `SourceFile`'s DIRECT children, so an inner class's `extends`
399/// (nested under `Class`/`ClassBody`) is never mistaken for the file's own. `None` if the file has no
400/// top-level `extends`.
401fn extends_decl_range(root: &GdNode) -> Option<TextRange> {
402    use gdscript_syntax::SyntaxKind;
403    for child in root.children() {
404        match child.kind() {
405            // Standalone `extends Target` — the whole clause is the anchor.
406            SyntaxKind::ExtendsClause => return Some(cst::text_range_of(child)),
407            // Inline `class_name Name extends Target` — anchor the `extends` keyword onward.
408            SyntaxKind::ClassNameDecl => {
409                if let Some(kw) = child.children().find(|c| c.kind() == SyntaxKind::ExtendsKw) {
410                    let start = cst::text_range_of(kw).start;
411                    let end = cst::text_range_of(child).end;
412                    return Some(TextRange::new(start, end));
413                }
414            }
415            _ => {}
416        }
417    }
418    None
419}
420
421/// Whether the file's `extends` inheritance chain transitively returns to itself (a genuine cycle).
422/// Walks base-by-base by `FileId` from `start`, stepping only across user `ScriptRef` bases (an
423/// engine `Object`/`Unknown` base ends the chain). A `FileId` revisit means a cycle. We stop as soon
424/// as we either revisit a file (cycle) or hit a non-script base (acyclic) — a deep but acyclic chain
425/// terminates without a revisit and is NOT flagged. Depth is also hard-capped as belt-and-suspenders
426/// (the visited set already guarantees termination).
427fn extends_chain_is_cyclic(db: &dyn Db, start: FileId) -> bool {
428    use std::collections::HashSet;
429    let mut visited: HashSet<FileId> = HashSet::new();
430    visited.insert(start);
431    let mut current = start;
432    for _ in 0..=64 {
433        let Some(file) = db.file_text(current) else {
434            return false;
435        };
436        let base = crate::queries::script_class(db, file).base().clone();
437        let Ty::ScriptRef(next) = base else {
438            return false; // engine `Object` / `Unknown` base — chain ends, no cycle.
439        };
440        let next_id = FileId(next.0);
441        if !visited.insert(next_id) {
442            // Revisiting an already-seen file closes a cycle. We report the cycle for every file ON
443            // it (each file's own `extends` is genuinely cyclic), so no need to special-case `start`.
444            return true;
445        }
446        current = next_id;
447    }
448    false
449}
450
451/// Infer a class field declaration as a single local-var statement (full annotation checks).
452fn unit_from_decl(
453    db: &dyn Db,
454    api: &EngineApi,
455    root: &GdNode,
456    class: &ClassScope,
457    ptr: AstPtr,
458    range: TextRange,
459) -> Option<Unit> {
460    let node = ptr.to_node(root)?;
461    let body = body::body_of_decl_stmt(&node);
462    let result = infer(db, api, root, class, &body, Ty::Variant);
463    Some(Unit {
464        range,
465        body,
466        result,
467    })
468}
469
470/// What type is expected of an expression (bidirectional checking).
471enum Expectation {
472    /// No expectation — pure synthesis.
473    None,
474    /// The expression is checked against this declared type.
475    Has(Ty),
476}
477
478struct Cx<'a> {
479    db: &'a dyn Db,
480    api: &'a EngineApi,
481    root: &'a GdNode,
482    body: &'a Body,
483    class: &'a ClassScope<'a>,
484    self_ty: Ty,
485    return_ty: Ty,
486    expr_ty: FxHashMap<ExprId, Ty>,
487    bindings: Vec<Binding>,
488    diagnostics: Vec<Diagnostic>,
489    /// Function-scoped local bindings (GDScript locals are function-, not block-, scoped).
490    locals: FxHashMap<SmolStr, Ty>,
491    /// Flow-scoped `is`/`as` narrowing facts, keyed by a dotted access path.
492    narrowing: FxHashMap<String, Ty>,
493}
494
495impl Cx<'_> {
496    // ---- small type constructors ----
497
498    fn builtin(&self, name: &str) -> Ty {
499        self.api
500            .builtin_by_name(name)
501            .map_or(Ty::Variant, Ty::Builtin)
502    }
503    fn int_ty(&self) -> Ty {
504        self.builtin("int")
505    }
506    fn float_ty(&self) -> Ty {
507        self.builtin("float")
508    }
509    fn bool_ty(&self) -> Ty {
510        self.builtin("bool")
511    }
512    fn is_int(&self, ty: &Ty) -> bool {
513        matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "int")
514    }
515    fn is_float(&self, ty: &Ty) -> bool {
516        matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "float")
517    }
518    fn is_numeric(&self, ty: &Ty) -> bool {
519        self.is_int(ty) || self.is_float(ty)
520    }
521
522    // ---- diagnostics ----
523
524    fn emit(&mut self, range: TextRange, severity: Severity, code: &str, message: String) {
525        self.diagnostics.push(Diagnostic {
526            range,
527            severity,
528            code: code.to_owned(),
529            message,
530            source: DiagnosticSource::Type,
531            fixes: Vec::new(),
532        });
533    }
534
535    fn range_of(&self, id: ExprId) -> TextRange {
536        self.body.source_map.expr_range(id)
537    }
538
539    /// Run `is_assignable(from, to)` and raise the matching diagnostic. Safe to call
540    /// unconditionally: `to` being `Variant`/`Unknown` yields `Ok`/no diagnostic.
541    fn check_assign(&mut self, from: &Ty, to: &Ty, range: TextRange) {
542        match ty::is_assignable(self.api, from, to) {
543            Assign::Narrowing => self.emit(
544                range,
545                Severity::Warning,
546                NARROWING_CONVERSION,
547                "Narrowing conversion (float is converted to int and loses precision).".to_owned(),
548            ),
549            Assign::No => {
550                let to_label = to.label(self.api).unwrap_or_else(|| "?".to_owned());
551                let from_label = from.label(self.api).unwrap_or_else(|| "?".to_owned());
552                self.emit(
553                    range,
554                    Severity::Error,
555                    TYPE_MISMATCH,
556                    format!(
557                        "Cannot assign a value of type \"{from_label}\" to a target of type \"{to_label}\"."
558                    ),
559                );
560            }
561            Assign::Ok | Assign::OkUnsafe | Assign::IntAsEnum => {}
562        }
563    }
564
565    // ---- statements ----
566
567    fn infer_block(&mut self, block: &[body::StmtId]) {
568        for &stmt in block {
569            self.infer_stmt(stmt);
570        }
571    }
572
573    fn infer_stmt(&mut self, id: body::StmtId) {
574        match self.body.stmt(id).clone() {
575            Stmt::Expr(e) => {
576                self.infer_expr(e, &Expectation::None);
577            }
578            Stmt::Var(v) => self.infer_local_var(&v),
579            Stmt::Return(e) => {
580                if let Some(e) = e {
581                    let expected = if self.return_ty.is_uninformative() {
582                        Expectation::None
583                    } else {
584                        Expectation::Has(self.return_ty.clone())
585                    };
586                    let t = self.infer_expr(e, &expected);
587                    if let Expectation::Has(ret) = expected {
588                        self.check_assign(&t, &ret, self.range_of(e));
589                    }
590                }
591            }
592            Stmt::If {
593                cond,
594                then_branch,
595                elifs,
596                else_branch,
597            } => {
598                self.infer_expr(cond, &Expectation::None);
599                self.in_branch(|cx| {
600                    cx.apply_narrowing(cond);
601                    cx.infer_block(&then_branch);
602                });
603                for (econd, eblock) in elifs {
604                    self.infer_expr(econd, &Expectation::None);
605                    self.in_branch(|cx| {
606                        cx.apply_narrowing(econd);
607                        cx.infer_block(&eblock);
608                    });
609                }
610                if let Some(eb) = else_branch {
611                    self.in_branch(|cx| cx.infer_block(&eb));
612                }
613            }
614            Stmt::While { cond, body } => {
615                self.infer_expr(cond, &Expectation::None);
616                self.in_branch(|cx| cx.infer_block(&body));
617            }
618            Stmt::For(f) => {
619                let iter_ty = self.infer_expr(f.iter, &Expectation::None);
620                let var_ty = f.var_type.as_ref().map_or_else(
621                    || self.loop_var_ty(&iter_ty),
622                    |ptr| self.resolve_ptr_ty(*ptr),
623                );
624                self.bindings.push(Binding {
625                    name_range: f.var_range,
626                    ty: var_ty.clone(),
627                    init: None,
628                    annotated: f.var_type.is_some(),
629                    inferred_colon_eq: false,
630                    kind: BindingKind::ForVar,
631                });
632                self.locals.insert(f.var.clone(), var_ty);
633                self.in_branch(|cx| cx.infer_block(&f.body));
634            }
635            Stmt::Match { scrutinee, arms } => {
636                self.infer_expr(scrutinee, &Expectation::None);
637                for arm in arms {
638                    self.in_branch(|cx| {
639                        for b in &arm.binds {
640                            // Record the capture as a binding so navigation (find-refs / rename)
641                            // sees it as a local that shadows a same-named member; the type is the
642                            // Phase-2 `Variant`.
643                            cx.bindings.push(Binding {
644                                name_range: b.range,
645                                ty: Ty::Variant,
646                                init: None,
647                                annotated: false,
648                                inferred_colon_eq: false,
649                                kind: BindingKind::MatchBind,
650                            });
651                            cx.locals.insert(b.name.clone(), Ty::Variant);
652                        }
653                        if let Some(g) = arm.guard {
654                            cx.infer_expr(g, &Expectation::None);
655                        }
656                        cx.infer_block(&arm.body);
657                    });
658                }
659            }
660            Stmt::Break | Stmt::Continue | Stmt::Pass => {}
661            Stmt::Assert(cond) => {
662                if let Some(cond) = cond {
663                    self.infer_expr(cond, &Expectation::None);
664                }
665            }
666        }
667    }
668
669    fn infer_local_var(&mut self, v: &body::LocalVar) {
670        let annotated = v.type_ref.map(|p| self.resolve_ptr_ty(p));
671        let init_ty = v.init.map(|e| {
672            let expected = annotated
673                .as_ref()
674                .map_or(Expectation::None, |t| Expectation::Has(t.clone()));
675            self.infer_expr(e, &expected)
676        });
677        let range = v.init.map_or(v.name_range, |e| self.range_of(e));
678
679        let binding_ty = match (&annotated, &init_ty) {
680            // `var x: T = e` — hard slot; check the initializer against it.
681            (Some(t), Some(init)) => {
682                self.check_assign(init, t, range);
683                t.clone()
684            }
685            // `var x: T` (no init).
686            (Some(t), None) => t.clone(),
687            // `var x := e` — inferred (hard); guard the Variant / null cases.
688            (None, Some(init)) if v.is_inferred => {
689                if init.is_variant() {
690                    self.emit(
691                        range,
692                        Severity::Error,
693                        INFERENCE_ON_VARIANT,
694                        inference_on_variant_msg(if v.is_const { "constant" } else { "variable" }),
695                    );
696                    Ty::Variant
697                } else {
698                    // `Unknown` (the seam) stays `Unknown` with no warning.
699                    init.clone()
700                }
701            }
702            // `var x = e` — untyped, soft → Variant. `const X = e` keeps the inferred type.
703            (None, Some(init)) => {
704                if v.is_const {
705                    init.clone()
706                } else {
707                    Ty::Variant
708                }
709            }
710            (None, None) => Ty::Variant,
711        };
712        self.bindings.push(Binding {
713            name_range: v.name_range,
714            ty: binding_ty.clone(),
715            init: v.init,
716            annotated: v.type_ref.is_some(),
717            inferred_colon_eq: v.is_inferred,
718            kind: BindingKind::Var,
719        });
720        self.narrowing.remove(v.name.as_str());
721        self.locals.insert(v.name.clone(), binding_ty);
722    }
723
724    // ---- expressions ----
725
726    fn infer_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
727        let ty = self.synth_expr(id, expected);
728        self.expr_ty.insert(id, ty.clone());
729        ty
730    }
731
732    #[allow(clippy::too_many_lines)]
733    fn synth_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
734        match self.body.expr(id).clone() {
735            Expr::Missing => Ty::Error,
736            Expr::Literal(lit) => self.literal_ty(lit),
737            Expr::Name(name) => self.resolve_name(id, &name),
738            Expr::SelfExpr => self.self_ty.clone(),
739            Expr::Super => self.class.base.clone(),
740            Expr::Paren(inner) => self.infer_expr(inner, expected),
741            Expr::Bin { op, lhs, rhs } => self.infer_bin(id, op, lhs, rhs),
742            Expr::Unary { op, operand } => {
743                let t = self.infer_expr(operand, &Expectation::None);
744                match op {
745                    UnOp::Not => self.bool_ty(),
746                    UnOp::BitNot => self.int_ty(),
747                    UnOp::Neg | UnOp::Pos => {
748                        if t.is_uninformative() || self.is_numeric(&t) {
749                            t
750                        } else {
751                            Ty::Variant
752                        }
753                    }
754                }
755            }
756            Expr::Ternary {
757                cond,
758                then_branch,
759                else_branch,
760            } => {
761                self.infer_expr(cond, &Expectation::None);
762                let a = self.infer_expr(then_branch, expected);
763                let b = self.infer_expr(else_branch, expected);
764                // A `null` branch does not poison the other: `x if c else null` is nullable-`x`.
765                if self.is_null(else_branch) {
766                    a
767                } else if self.is_null(then_branch) {
768                    b
769                } else {
770                    self.join(&a, &b)
771                }
772            }
773            Expr::Call { callee, args } => self.infer_call(callee, &args),
774            Expr::Field {
775                receiver,
776                name,
777                name_range,
778            } => {
779                self.infer_field(receiver, &name, name_range, /*as_method=*/ false)
780            }
781            Expr::Index { base, index } => {
782                let base_ty = self.infer_expr(base, &Expectation::None);
783                self.infer_expr(index, &Expectation::None);
784                self.index_ty(&base_ty)
785            }
786            Expr::Is { operand, .. } => {
787                self.infer_expr(operand, &Expectation::None);
788                self.bool_ty()
789            }
790            Expr::Cast { operand, ty } => {
791                self.infer_expr(operand, &Expectation::None);
792                ty.map_or(Ty::Variant, |p| self.resolve_ptr_ty(p))
793            }
794            Expr::In { lhs, rhs, .. } => {
795                self.infer_expr(lhs, &Expectation::None);
796                self.infer_expr(rhs, &Expectation::None);
797                self.bool_ty()
798            }
799            Expr::Await(operand) => {
800                let operand_ty = self.infer_expr(operand, &Expectation::None);
801                // `await coroutine()` yields the call's value, so await is **identity** on the operand
802                // type (`await f()` for `func f() -> int` is `int`) — recovered here. `await signal`
803                // instead yields the signal's emitted payload, which needs the Phase-3+ signal-signature
804                // table; until then it's the seam (never `Variant`, so `var x := await sig` never warns).
805                if matches!(operand_ty, Ty::Signal(_)) {
806                    Ty::Unknown
807                } else {
808                    operand_ty
809                }
810            }
811            Expr::Array(elems) => {
812                // Checking mode: an expected `Array[T]` is pushed down onto the literal (so
813                // `var a: Array[String] = []` / `[...]` is accepted). Otherwise the engine does
814                // not infer a literal's element type past `Variant`.
815                let pushed = match expected {
816                    Expectation::Has(Ty::Array(e)) => Some((**e).clone()),
817                    _ => None,
818                };
819                let elem_exp = pushed.clone().map_or(Expectation::None, Expectation::Has);
820                for e in elems {
821                    self.infer_expr(e, &elem_exp);
822                }
823                pushed.map_or_else(Ty::array_of_variant, |e| Ty::Array(Box::new(e)))
824            }
825            Expr::Dict(entries) => {
826                let pushed = match expected {
827                    Expectation::Has(Ty::Dict(k, v)) => Some(((**k).clone(), (**v).clone())),
828                    _ => None,
829                };
830                let (kx, vx) = pushed
831                    .clone()
832                    .map_or((Expectation::None, Expectation::None), |(k, v)| {
833                        (Expectation::Has(k), Expectation::Has(v))
834                    });
835                for (k, v) in entries {
836                    self.infer_expr(k, &kx);
837                    if let Some(v) = v {
838                        self.infer_expr(v, &vx);
839                    }
840                }
841                pushed.map_or_else(Ty::dict_of_variant, |(k, v)| {
842                    Ty::Dict(Box::new(k), Box::new(v))
843                })
844            }
845            Expr::Lambda { params, body } => {
846                self.infer_lambda(&params, &body);
847                Ty::Callable
848            }
849            Expr::Preload { arg, path } => {
850                if let Some(arg) = arg {
851                    self.infer_expr(arg, &Expectation::None);
852                }
853                // A constant string-literal path resolves to the declaring file's `ScriptRef`
854                // (M3 — a SCRIPT meta-type in Godot; `X.new()`/`X.member` then resolve via the
855                // usual `ScriptRef` walk). A non-constant argument (`preload(var)`) — which Godot
856                // itself rejects — stays the seam, never a false diagnostic.
857                match path {
858                    // Anchor a relative `preload("sibling.gd")` to the importing file's directory
859                    // before resolving (Godot anchors relative resource paths); absolute paths pass
860                    // through, and a relative path with no anchor stays the seam.
861                    Some(p) => {
862                        match resolve::anchor_res_path(self.self_res_path().as_deref(), &p) {
863                            Some(abs) => resolve::resolve_external(
864                                self.db,
865                                &resolve::ExternalRef::Preload(abs),
866                            ),
867                            None => Ty::Unknown,
868                        }
869                    }
870                    None => Ty::Unknown,
871                }
872            }
873            // `$Path`/`%Unique` — resolve the literal path against the owning scene to the node's
874            // concrete type (Phase-4 M1); a computed/unresolvable path stays `Object(Node)`.
875            Expr::GetNode { path, unique } => self.resolve_node_path(id, path.as_deref(), unique),
876        }
877    }
878
879    /// Whether `id` is the `null` literal.
880    fn is_null(&self, id: ExprId) -> bool {
881        matches!(self.body.expr(id), Expr::Literal(Literal::Null))
882    }
883
884    fn literal_ty(&self, lit: Literal) -> Ty {
885        match lit {
886            Literal::Int => self.int_ty(),
887            Literal::Float | Literal::MathConst => self.float_ty(),
888            Literal::Bool => self.bool_ty(),
889            Literal::Str => self.builtin("String"),
890            Literal::StringName => self.builtin("StringName"),
891            Literal::NodePath => self.builtin("NodePath"),
892            // `null` is compatible everywhere; typing it `Variant` avoids false mismatches.
893            Literal::Null => Ty::Variant,
894        }
895    }
896
897    fn node_ty(&self) -> Ty {
898        self.api
899            .class_by_name("Node")
900            .map_or(Ty::Unknown, Ty::Object)
901    }
902
903    // ---- scene-aware node-path typing (Phase-4 M1) ----
904
905    /// Resolve a `$Path`/`%Unique`/`get_node("…")` literal node path against the owning scene to the
906    /// node's concrete type. A computed (`None`) path, no owning scene, an `..`/absolute escape, or a
907    /// path that descends into an instanced sub-scene all degrade to `Object(Node)` — never a false
908    /// positive. A *genuinely* absent in-scene node raises `INVALID_NODE_PATH` (M2), but only when
909    /// the script attaches to exactly one scene (an ambiguous multi-scene attachment stays silent).
910    fn resolve_node_path(&mut self, id: ExprId, path: Option<&str>, unique: bool) -> Ty {
911        use gdscript_scene::NodePathResolution as R;
912        let fallback = self.node_ty();
913        let Some(path) = path else {
914            return fallback; // computed `get_node(var)` — stays `Node`
915        };
916        let Some(ctx) = self.owning_scene() else {
917            return fallback; // no scene attaches this script (dynamic UI / single-file)
918        };
919        let resolution = if unique {
920            ctx.model.classify_unique(path)
921        } else {
922            ctx.model.classify_path_from(ctx.attach, path)
923        };
924        match resolution {
925            R::Resolved(idx) => ctx
926                .model
927                .node(idx)
928                .and_then(|n| self.scene_node_ty(&ctx.model, n, 0))
929                .unwrap_or(fallback),
930            R::Missing if !ctx.ambiguous => {
931                let what = if unique { "unique name" } else { "node path" };
932                let sigil = if unique { "%" } else { "$" };
933                self.emit(
934                    self.range_of(id),
935                    Severity::Warning,
936                    INVALID_NODE_PATH,
937                    format!("no {what} `{sigil}{path}` in the owning scene"),
938                );
939                fallback
940            }
941            // ambiguous miss / escape (`..`/absolute) / into-instance → `Node`, never a false warning
942            _ => fallback,
943        }
944    }
945
946    /// The owning-scene context for the current file (scene + attach node + multi-scene ambiguity).
947    /// Recovered from `self_ty`, which `analyze_file` sets to the file's own `ScriptRef` (so no extra
948    /// `FileId` threading).
949    fn owning_scene(&self) -> Option<crate::queries::SceneContext> {
950        let Ty::ScriptRef(sref) = &self.self_ty else {
951            return None;
952        };
953        let ft = self.db.file_text(FileId(sref.0))?;
954        crate::queries::scene_context(self.db, ft)
955    }
956
957    /// The importing file's own `res://` path (from `self_ty`), for anchoring relative
958    /// `preload`/`extends` paths to its directory. `None` when the file has no resource path.
959    fn self_res_path(&self) -> Option<SmolStr> {
960        let Ty::ScriptRef(sref) = &self.self_ty else {
961            return None;
962        };
963        self.db.file_text(FileId(sref.0))?.res_path(self.db)
964    }
965
966    /// The concrete `Ty` of a scene node, by precedence: an attached script's own class (most
967    /// specific) wins; else the declared `type=` (native class or `class_name`); else — an instanced
968    /// node (`instance=`, no own `type=`/script) — the **instanced sub-scene's root** type (M3,
969    /// recursive). `None` for a node we can't sharpen (the caller degrades to `Node`).
970    fn scene_node_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
971        if let Some(script_ty) = self.node_script_ref(scene, node) {
972            return Some(script_ty);
973        }
974        if let Some(decl) = node.decl_type.as_ref() {
975            let ty = resolve::resolve_type_name(self.db, self.api, decl);
976            if !ty.is_uninformative() {
977                return Some(ty);
978            }
979        }
980        self.instance_root_ty(scene, node, depth)
981    }
982
983    /// An instanced node (`instance=ExtResource(id)`) takes the type of the instanced sub-scene's
984    /// ROOT node — resolved recursively, so the root's own script / `type=` / nested instance all
985    /// flow through (so `$Enemy` types as `enemy.tscn`'s root class, not bare `Node`). Depth-bounded
986    /// against an instancing cycle (scene A instances B instances A).
987    fn instance_root_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
988        if depth >= 16 {
989            return None;
990        }
991        let inst = node.instance.as_ref()?;
992        let path = scene.ext_resources.get(inst)?.path.as_ref()?;
993        let root = self.db.source_root()?;
994        let file = crate::queries::res_path_registry(self.db, root)
995            .get(path.as_str())
996            .copied()?;
997        let ft = self.db.file_text(file)?;
998        let sub = crate::queries::scene_model(self.db, ft);
999        let root_node = sub.node(sub.root?)?;
1000        self.scene_node_ty(&sub, root_node, depth + 1)
1001    }
1002
1003    /// The `ScriptRef` of a node's attached `.gd` script (`script = ExtResource(id)` → its `res://`
1004    /// path → `FileId`), or `None` if it has no resolvable external script.
1005    fn node_script_ref(&self, scene: &SceneModel, node: &SceneNode) -> Option<Ty> {
1006        let path = scene
1007            .ext_resources
1008            .get(node.script.as_ref()?)?
1009            .path
1010            .as_ref()?;
1011        let root = self.db.source_root()?;
1012        let file = crate::queries::res_path_registry(self.db, root)
1013            .get(path.as_str())
1014            .copied()?;
1015        Some(Ty::ScriptRef(ScriptRefId(file.0)))
1016    }
1017
1018    fn infer_bin(&mut self, id: ExprId, op: BinOp, lhs: ExprId, rhs: ExprId) -> Ty {
1019        if op == BinOp::Assign {
1020            return self.infer_assign(lhs, rhs);
1021        }
1022        let lt = self.infer_expr(lhs, &Expectation::None);
1023        let rt = self.infer_expr(rhs, &Expectation::None);
1024        if op.is_boolean() {
1025            return self.bool_ty();
1026        }
1027        // `int / int` discards the fractional part.
1028        if op == BinOp::Div && self.is_int(&lt) && self.is_int(&rt) {
1029            self.emit(
1030                self.range_of(id),
1031                Severity::Warning,
1032                INTEGER_DIVISION,
1033                "Integer division. Decimal part will be discarded.".to_owned(),
1034            );
1035            return self.int_ty();
1036        }
1037        self.bin_result(op, &lt, &rt)
1038    }
1039
1040    fn infer_assign(&mut self, lhs: ExprId, rhs: ExprId) -> Ty {
1041        let slot = self.infer_expr(lhs, &Expectation::None);
1042        let expected = if slot.is_uninformative() {
1043            Expectation::None
1044        } else {
1045            Expectation::Has(slot.clone())
1046        };
1047        let value = self.infer_expr(rhs, &expected);
1048        if !slot.is_uninformative() {
1049            self.check_assign(&value, &slot, self.range_of(rhs));
1050        }
1051        // Assignment narrowing: bound the narrowed type by the declared slot.
1052        if let Some(key) = self.narrow_key(lhs) {
1053            let narrowed = if slot.is_uninformative() {
1054                value.clone()
1055            } else {
1056                slot.clone()
1057            };
1058            self.narrowing.insert(key, narrowed);
1059        }
1060        slot
1061    }
1062
1063    /// Resolve a binary operator's result type via the builtin operator table, with a numeric
1064    /// fallback. Comparison/logical operators are handled by the caller.
1065    fn bin_result(&self, op: BinOp, lt: &Ty, rt: &Ty) -> Ty {
1066        if let (Ty::Builtin(b), Some(sym)) = (lt, op_symbol(op)) {
1067            for o in self.api.builtin_operators(*b) {
1068                if o.op == sym
1069                    && let Some(right) = &o.right
1070                    && self.tyref_matches(right, rt)
1071                {
1072                    return ty::resolve_tyref(self.api, &o.result);
1073                }
1074            }
1075        }
1076        if self.is_numeric(lt) && self.is_numeric(rt) {
1077            return if self.is_float(lt) || self.is_float(rt) {
1078                self.float_ty()
1079            } else {
1080                self.int_ty()
1081            };
1082        }
1083        // A seam operand keeps the result on the seam (`a + unknown` is `Unknown`, not the
1084        // gradual `Variant`, so `var x := a + unknown` never warns).
1085        if lt.is_unknown() || rt.is_unknown() || lt.is_error() || rt.is_error() {
1086            return Ty::Unknown;
1087        }
1088        Ty::Variant
1089    }
1090
1091    fn tyref_matches(&self, tyref: &TyRef, ty: &Ty) -> bool {
1092        let resolved = ty::resolve_tyref(self.api, tyref);
1093        resolved.is_variant() || &resolved == ty
1094    }
1095
1096    fn infer_call(&mut self, callee: ExprId, args: &[ExprId]) -> Ty {
1097        // Argument expressions are always inferred (their own diagnostics + hover).
1098        for &a in args {
1099            self.infer_expr(a, &Expectation::None);
1100        }
1101        let ret = match self.body.expr(callee).clone() {
1102            Expr::Field {
1103                receiver,
1104                name,
1105                name_range,
1106            } => {
1107                self.infer_field(receiver, &name, name_range, /*as_method=*/ true)
1108            }
1109            Expr::Name(name) => {
1110                let ret = self.resolve_call_name(&name);
1111                self.expr_ty.insert(callee, Ty::Callable);
1112                ret
1113            }
1114            // Calling an arbitrary expression — a `Callable` value or an immediately-invoked
1115            // lambda (`(func(): …).call()`): the callee's return type isn't tracked, so the
1116            // result is the seam (not `Variant`), and `var x := f()()` never warns.
1117            _ => {
1118                self.infer_expr(callee, &Expectation::None);
1119                Ty::Unknown
1120            }
1121        };
1122        // UNSAFE_CALL_ARGUMENT (Phase-2 §5): args + receiver are now inferred (in `expr_ty`), so
1123        // check each argument against the statically-resolved callee's parameter types.
1124        self.check_call_args(callee, args);
1125        ret
1126    }
1127
1128    /// Raise `UNSAFE_CALL_ARGUMENT` for each argument whose static type needs an unsafe implicit
1129    /// cast (`Variant` / a downcast) into the resolved parameter type — Godot's per-argument
1130    /// value-prop warning. Only fires when the callee resolves to a concrete signature here; an
1131    /// uninformative argument (the cross-file seam) is `Assign::Ok` and correctly silent, and an
1132    /// untyped parameter accepts anything.
1133    fn check_call_args(&mut self, callee: ExprId, args: &[ExprId]) {
1134        let Some(params) = self.call_param_tys(callee) else {
1135            return;
1136        };
1137        for (i, &arg) in args.iter().enumerate() {
1138            let Some(param_ty) = params.get(i) else {
1139                break; // a vararg tail or an arity mismatch — not an argument-type concern
1140            };
1141            if param_ty.is_uninformative() || param_ty.is_variant() {
1142                continue; // an untyped parameter accepts anything safely
1143            }
1144            // A missing arg type defaults to the seam (never warns), not `Variant` (would warn).
1145            let arg_ty = self.expr_ty.get(&arg).cloned().unwrap_or(Ty::Unknown);
1146            if ty::is_assignable(self.api, &arg_ty, param_ty) == Assign::OkUnsafe {
1147                let pl = param_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
1148                let al = arg_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
1149                self.emit(
1150                    self.range_of(arg),
1151                    Severity::Warning,
1152                    UNSAFE_CALL_ARGUMENT,
1153                    format!(
1154                        "The argument {} requires a value of type \"{pl}\" but is passed \"{al}\", which is unsafe.",
1155                        i + 1
1156                    ),
1157                );
1158            }
1159        }
1160    }
1161
1162    /// Parameter types of a statically-resolved callee, for [`Self::check_call_args`]. `None` when
1163    /// the callee isn't concretely resolvable here (a cross-file script method — params aren't
1164    /// modeled —, a builtin/utility, a `Callable` value): those raise no argument warning.
1165    fn call_param_tys(&self, callee: ExprId) -> Option<Vec<Ty>> {
1166        match self.body.expr(callee) {
1167            Expr::Name(name) => self.name_call_param_tys(name),
1168            Expr::Field { receiver, name, .. } => match self.expr_ty.get(receiver)? {
1169                Ty::Object(class) => match self.api.lookup_member(*class, name)? {
1170                    MemberRef::Method(sig) => Some(
1171                        sig.params
1172                            .iter()
1173                            .map(|p| ty::resolve_tyref(self.api, &p.ty))
1174                            .collect(),
1175                    ),
1176                    _ => None,
1177                },
1178                // ScriptRef / builtin / seam receivers: params not uniformly modeled — skip.
1179                _ => None,
1180            },
1181            _ => None,
1182        }
1183    }
1184
1185    /// Parameter types for a bare-name call (`foo(...)` / an inherited `method(...)`): an own `func`
1186    /// first, then the `self` engine base's method. Utilities/builtins are skipped (looser, often
1187    /// variadic typing — out of the conservative MVP slice).
1188    fn name_call_param_tys(&self, name: &str) -> Option<Vec<Ty>> {
1189        if let Some(item) = self.class.lookup(name)
1190            && let Some(Member::Func(f)) = self.class.member(item)
1191        {
1192            return Some(
1193                f.params
1194                    .iter()
1195                    .map(|p| {
1196                        p.type_ref.as_deref().map_or(Ty::Variant, |t| {
1197                            resolve::resolve_type_name(self.db, self.api, t)
1198                        })
1199                    })
1200                    .collect(),
1201            );
1202        }
1203        if let Ty::Object(base) = self.class.base
1204            && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
1205        {
1206            return Some(
1207                sig.params
1208                    .iter()
1209                    .map(|p| ty::resolve_tyref(self.api, &p.ty))
1210                    .collect(),
1211            );
1212        }
1213        None
1214    }
1215
1216    /// Resolve a bare-name call (`foo(...)`): own method → utility/builtin fn → constructor.
1217    fn resolve_call_name(&self, name: &str) -> Ty {
1218        if let Some(item) = self.class.lookup(name)
1219            && let Some(Member::Func(f)) = self.class.member(item)
1220        {
1221            return self.func_return_ty(f.return_type.as_deref());
1222        }
1223        // A bare call inside the class is `self.name(...)` — resolve against the inherited base.
1224        if let Ty::Object(base) = self.class.base
1225            && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
1226        {
1227            return ty::resolve_tyref(self.api, &sig.return_ty);
1228        }
1229        if let Some(u) = self.api.utility(name) {
1230            return ty::resolve_tyref(self.api, &u.return_ty);
1231        }
1232        if let Some(f) = self.api.gdscript_builtin(name) {
1233            return resolve::layer_to_ty(self.api, f.ret);
1234        }
1235        // A builtin / class name used as a constructor: `Vector2(...)` / `Array(...)`.
1236        // Normalize via `resolve_tyref` so `Array`/`Dictionary`/`Callable`/`Signal` land on
1237        // their dedicated `Ty` variants rather than `Builtin(...)`.
1238        if let Some(b) = self.api.builtin_by_name(name) {
1239            return ty::resolve_tyref(self.api, &TyRef::Builtin(b));
1240        }
1241        // Otherwise unresolved — most likely a cross-file global / autoload / a method on a
1242        // `class_name` base we can't see. Treat as the seam so `var x := foo()` never warns.
1243        Ty::Unknown
1244    }
1245
1246    fn func_return_ty(&self, annotation: Option<&str>) -> Ty {
1247        annotation.map_or(Ty::Variant, |t| {
1248            resolve::resolve_type_name(self.db, self.api, t)
1249        })
1250    }
1251
1252    /// Member access `receiver.name`. When `as_method`, resolve a method (and use its return
1253    /// type); otherwise resolve a property/const/etc. Raises `UNSAFE_*` only on a statically
1254    /// **known** receiver.
1255    fn infer_field(
1256        &mut self,
1257        receiver: ExprId,
1258        name: &str,
1259        name_range: TextRange,
1260        as_method: bool,
1261    ) -> Ty {
1262        let is_self = matches!(self.body.expr(receiver), Expr::SelfExpr);
1263        let recv_ty = self.infer_expr(receiver, &Expectation::None);
1264
1265        // `self.member` consults this file's own members first (Playbook §3.2).
1266        if is_self && let Some(item) = self.class.lookup(name) {
1267            return self.own_member_ty(item, as_method);
1268        }
1269
1270        match &recv_ty {
1271            // Uninformative receivers are unchecked and **propagate the seam**: a member of an
1272            // `Unknown` (cross-file) value is itself `Unknown` (never warns), a member of a
1273            // `Variant` is `Variant`, of an `Error` is `Error`. Collapsing `Unknown` to
1274            // `Variant` here would wrongly fire `INFERENCE_ON_VARIANT` on `var x := other.field`.
1275            t if t.is_uninformative() => recv_ty.clone(),
1276            Ty::Object(class) => {
1277                if name == "new" {
1278                    // `Class.new(...)` always constructs an instance of the class (some classes,
1279                    // e.g. GDScript, also carry a modeled `new` member — the constructor wins).
1280                    recv_ty.clone()
1281                } else if let Some(m) = self.api.lookup_member(*class, name) {
1282                    self.member_ref_ty(&m, as_method)
1283                } else if let Some(t) = self.class_enum_value(*class, name) {
1284                    // A statically-accessed enum value (`Control.PRESET_FULL_RECT`).
1285                    t
1286                } else {
1287                    // Self with an Object base already checked own members above.
1288                    self.emit_unsafe(name, &recv_ty, name_range, as_method);
1289                    Ty::Variant
1290                }
1291            }
1292            Ty::Builtin(_) | Ty::Array(_) | Ty::Dict(..) | Ty::Callable | Ty::Signal(_) => {
1293                self.builtin_member_ty(&recv_ty, name, name_range, as_method)
1294            }
1295            // Enum value access (`MyEnum.VALUE`) is an `int`.
1296            Ty::Enum(_) => self.int_ty(),
1297            // A cross-file script reference: resolve the member against its (own) member table.
1298            Ty::ScriptRef(sref) => self.script_member_ty(*sref, name, as_method),
1299            _ => Ty::Variant,
1300        }
1301    }
1302
1303    /// A member of a cross-file script (`ScriptRef`): looked up in the script's own member table
1304    /// (M1). A member we don't model — e.g. one inherited from a base we don't resolve until M2 —
1305    /// yields the seam (`Unknown`), **never** an `UNSAFE_*` warning. `Class.new(...)` constructs
1306    /// an instance of the class.
1307    fn script_member_ty(&self, sref: ScriptRefId, name: &str, as_method: bool) -> Ty {
1308        if name == "new" {
1309            return Ty::ScriptRef(sref);
1310        }
1311        self.script_member_walk(sref, name, as_method, 0)
1312            .unwrap_or(Ty::Unknown)
1313    }
1314
1315    /// Walk a script class's `extends` chain for `name`: own members first, then a user base
1316    /// (another `ScriptRef`), then an engine base (the API table). Depth-bounded so a cyclic
1317    /// `extends` cannot loop. `None` = not found anywhere in the chain (the seam).
1318    fn script_member_walk(
1319        &self,
1320        sref: ScriptRefId,
1321        name: &str,
1322        as_method: bool,
1323        depth: u32,
1324    ) -> Option<Ty> {
1325        if depth > 32 {
1326            return None;
1327        }
1328        let file = self.db.file_text(FileId(sref.0))?;
1329        let sc = crate::queries::script_class(self.db, file);
1330        if let Some(m) = sc.member(name) {
1331            return Some(match m {
1332                crate::queries::MemberSig::Method(ret) => {
1333                    if as_method {
1334                        ret.clone()
1335                    } else {
1336                        Ty::Callable
1337                    }
1338                }
1339                crate::queries::MemberSig::Field(t) => t.clone(),
1340                crate::queries::MemberSig::Signal => Ty::Signal(None),
1341            });
1342        }
1343        // Not an own member — continue up the inheritance chain.
1344        match sc.base() {
1345            Ty::ScriptRef(base) => self.script_member_walk(*base, name, as_method, depth + 1),
1346            Ty::Object(class) => self
1347                .api
1348                .lookup_member(*class, name)
1349                .map(|m| self.member_ref_ty(&m, as_method)),
1350            _ => None,
1351        }
1352    }
1353
1354    /// Whether a value of type `sub` is statically a subtype of `sup` — composing user `ScriptRef`
1355    /// `extends` chains with the engine class table (M4, for `is`/`as` widen-only narrowing). A
1356    /// `ScriptRef` IS-A its native base (so `script_value is Node` holds), but Godot's asymmetry is
1357    /// honored: a native/script value is **not** a subtype of an *unrelated* user script.
1358    fn is_subtype(&self, sub: &Ty, sup: &Ty) -> bool {
1359        match (sub, sup) {
1360            (Ty::Object(a), Ty::Object(b)) => self.api.is_subclass(*a, *b),
1361            (Ty::ScriptRef(a), Ty::ScriptRef(b)) => self.script_is_subtype(*a, *b, 0),
1362            (Ty::ScriptRef(a), Ty::Object(b)) => self.script_extends_engine(*a, *b, 0),
1363            _ => false,
1364        }
1365    }
1366
1367    /// Whether script `sub` is `sup` or transitively extends it — walk the `extends` base chain by
1368    /// script identity (depth-bounded, like [`script_member_walk`](Self::script_member_walk)).
1369    fn script_is_subtype(&self, sub: ScriptRefId, sup: ScriptRefId, depth: u32) -> bool {
1370        if depth > 32 {
1371            return false;
1372        }
1373        if sub == sup {
1374            return true;
1375        }
1376        let Some(file) = self.db.file_text(FileId(sub.0)) else {
1377            return false;
1378        };
1379        match crate::queries::script_class(self.db, file).base() {
1380            Ty::ScriptRef(base) => self.script_is_subtype(*base, sup, depth + 1),
1381            _ => false,
1382        }
1383    }
1384
1385    /// Whether script `sub`'s `extends` chain reaches engine class `sup_native` at its native base.
1386    fn script_extends_engine(
1387        &self,
1388        sub: ScriptRefId,
1389        sup_native: gdscript_api::ClassId,
1390        depth: u32,
1391    ) -> bool {
1392        if depth > 32 {
1393            return false;
1394        }
1395        let Some(file) = self.db.file_text(FileId(sub.0)) else {
1396            return false;
1397        };
1398        match crate::queries::script_class(self.db, file).base() {
1399            Ty::ScriptRef(base) => self.script_extends_engine(*base, sup_native, depth + 1),
1400            Ty::Object(native) => self.api.is_subclass(*native, sup_native),
1401            _ => false,
1402        }
1403    }
1404
1405    fn emit_unsafe(&mut self, name: &str, recv: &Ty, range: TextRange, as_method: bool) {
1406        let recv_label = recv.label(self.api).unwrap_or_else(|| "?".to_owned());
1407        let (code, message) = if as_method {
1408            (
1409                UNSAFE_METHOD_ACCESS,
1410                format!(
1411                    "The method \"{name}()\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1412                ),
1413            )
1414        } else {
1415            (
1416                UNSAFE_PROPERTY_ACCESS,
1417                format!(
1418                    "The property \"{name}\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1419                ),
1420            )
1421        };
1422        self.emit(range, Severity::Warning, code, message);
1423    }
1424
1425    fn member_ref_ty(&self, m: &MemberRef, as_method: bool) -> Ty {
1426        match m {
1427            MemberRef::Method(sig) => {
1428                if as_method {
1429                    ty::resolve_tyref(self.api, &sig.return_ty)
1430                } else {
1431                    Ty::Callable
1432                }
1433            }
1434            MemberRef::Property(p) => p.enum_of.as_ref().map_or_else(
1435                || ty::resolve_tyref(self.api, &p.ty),
1436                |q| {
1437                    Ty::Enum(EnumRef {
1438                        qualified: SmolStr::new(q),
1439                        bitfield: false,
1440                    })
1441                },
1442            ),
1443            MemberRef::Const(c) => ty::resolve_tyref(self.api, &c.ty),
1444            MemberRef::Signal(_) => Ty::Signal(None),
1445            MemberRef::Enum(_) => Ty::Variant,
1446        }
1447    }
1448
1449    fn builtin_member_ty(
1450        &mut self,
1451        recv: &Ty,
1452        name: &str,
1453        range: TextRange,
1454        as_method: bool,
1455    ) -> Ty {
1456        let Some(bid) = self.builtin_id_of(recv) else {
1457            return Ty::Variant;
1458        };
1459        if as_method {
1460            return if let Some(sig) = self.api.builtin_method(bid, name) {
1461                ty::resolve_tyref(self.api, &sig.return_ty)
1462            } else {
1463                self.emit_unsafe(name, recv, range, true);
1464                Ty::Variant
1465            };
1466        }
1467        if let Some(member) = self.api.builtin_member(bid, name) {
1468            return ty::resolve_tyref(self.api, &member.ty);
1469        }
1470        // Static constants (`Vector2.ZERO`, `Color.WHITE`) and enum values (`Variant.Type.*`).
1471        let data = self.api.builtin(bid);
1472        if let Some(c) = data.constants.iter().find(|c| c.name == name) {
1473            return ty::resolve_tyref(self.api, &c.ty);
1474        }
1475        if data
1476            .enums
1477            .iter()
1478            .any(|e| e.values.iter().any(|v| v.name == name))
1479        {
1480            return self.int_ty();
1481        }
1482        if self.api.builtin_method(bid, name).is_some() {
1483            return Ty::Callable;
1484        }
1485        self.emit_unsafe(name, recv, range, false);
1486        Ty::Variant
1487    }
1488
1489    /// The type of a class enum **value** accessed statically (`Control.PRESET_FULL_RECT`):
1490    /// the engine exposes enum values as class members, so search every (inherited) enum's
1491    /// values. Returns `int` when found.
1492    fn class_enum_value(&self, class: gdscript_api::ClassId, name: &str) -> Option<Ty> {
1493        let mut cur = Some(class);
1494        while let Some(cid) = cur {
1495            let c = self.api.class(cid);
1496            if c.enums
1497                .iter()
1498                .any(|e| e.values.iter().any(|v| v.name == name))
1499            {
1500                return Some(self.int_ty());
1501            }
1502            cur = c.base;
1503        }
1504        None
1505    }
1506
1507    /// The builtin id backing a builtin / `Array` / `Dictionary` receiver.
1508    fn builtin_id_of(&self, ty: &Ty) -> Option<gdscript_api::BuiltinId> {
1509        match ty {
1510            Ty::Builtin(b) => Some(*b),
1511            Ty::Array(_) => self.api.builtin_by_name("Array"),
1512            Ty::Dict(..) => self.api.builtin_by_name("Dictionary"),
1513            Ty::Callable => self.api.builtin_by_name("Callable"),
1514            Ty::Signal(_) => self.api.builtin_by_name("Signal"),
1515            _ => None,
1516        }
1517    }
1518
1519    /// The element type of an indexing expression (Playbook §2 switch).
1520    fn index_ty(&self, base: &Ty) -> Ty {
1521        match base {
1522            Ty::Array(elem) => (**elem).clone(),
1523            Ty::Builtin(b) => self
1524                .api
1525                .builtin(*b)
1526                .indexing_return
1527                .as_ref()
1528                .map_or(Ty::Variant, |r| ty::resolve_tyref(self.api, r)),
1529            // Indexing through the seam stays on the seam (never warns).
1530            Ty::Unknown => Ty::Unknown,
1531            Ty::Error => Ty::Error,
1532            _ => Ty::Variant,
1533        }
1534    }
1535
1536    /// The loop variable's type for `for v in iter:` (Playbook §2 switch).
1537    fn loop_var_ty(&self, iter: &Ty) -> Ty {
1538        match iter {
1539            Ty::Array(elem) => (**elem).clone(),
1540            Ty::Builtin(b) => {
1541                let data = self.api.builtin(*b);
1542                if data.name == "int" {
1543                    // `for i in 5` / `for i in range(...)` → int.
1544                    self.int_ty()
1545                } else if let Some(r) = &data.indexing_return {
1546                    // `for c in "abc"` → String; `for s in packed_string_array` → String; …
1547                    ty::resolve_tyref(self.api, r)
1548                } else {
1549                    Ty::Variant
1550                }
1551            }
1552            // Iterating a seam value keeps the loop var on the seam (never warns).
1553            Ty::Unknown => Ty::Unknown,
1554            Ty::Error => Ty::Error,
1555            _ => Ty::Variant,
1556        }
1557    }
1558
1559    fn infer_lambda(&mut self, params: &[ParamBinding], body: &[body::StmtId]) {
1560        // Lambda params shadow within the body; restore the outer locals afterward. A `return`
1561        // inside the lambda is the *lambda's* return, not the enclosing function's — so disable
1562        // return checking (set the expected return to `Variant`) while walking the body.
1563        let saved_locals = self.locals.clone();
1564        let saved_ret = std::mem::replace(&mut self.return_ty, Ty::Variant);
1565        for p in params {
1566            let ty = self.param_ty(p);
1567            self.bindings.push(Binding {
1568                name_range: p.name_range,
1569                ty: ty.clone(),
1570                init: None,
1571                annotated: p.type_ref.is_some(),
1572                inferred_colon_eq: false,
1573                kind: BindingKind::Param,
1574            });
1575            self.locals.insert(p.name.clone(), ty);
1576        }
1577        self.infer_block(body);
1578        self.return_ty = saved_ret;
1579        self.locals = saved_locals;
1580    }
1581
1582    fn param_ty(&mut self, p: &ParamBinding) -> Ty {
1583        if let Some(ptr) = p.type_ref {
1584            return self.resolve_ptr_ty(ptr);
1585        }
1586        // An unannotated param infers from its default, else `Variant`.
1587        p.default
1588            .map_or(Ty::Variant, |e| self.infer_expr(e, &Expectation::None))
1589    }
1590
1591    // ---- name resolution (local → class member → inherited → global) ----
1592
1593    fn resolve_name(&mut self, id: ExprId, name: &str) -> Ty {
1594        // Flow narrowing wins over the binding's declared type.
1595        if let Some(key) = self.narrow_key(id)
1596            && let Some(t) = self.narrowing.get(&key)
1597        {
1598            return t.clone();
1599        }
1600        if let Some(t) = self.locals.get(name) {
1601            return t.clone();
1602        }
1603        if let Some(item) = self.class.lookup(name) {
1604            return self.own_member_ty(item, false);
1605        }
1606        // Inherited members: an engine `Object` base via the API table, or a user `ScriptRef`
1607        // base via the script member walk (M2 — so a class extending another class_name sees its
1608        // inherited members).
1609        match self.class.base.clone() {
1610            Ty::Object(base) => {
1611                if let Some(m) = self.api.lookup_member(base, name) {
1612                    return self.member_ref_ty(&m, false);
1613                }
1614            }
1615            Ty::ScriptRef(base) => {
1616                if let Some(t) = self.script_member_walk(base, name, false, 0) {
1617                    return t;
1618                }
1619            }
1620            _ => {}
1621        }
1622        if let Some(g) = resolve::resolve_global(self.api, name) {
1623            return global_ty(&g);
1624        }
1625        // A project-global `class_name` used as a value — the class itself, for static access
1626        // (`V.fc()`) or as a constructor (`Player.new()`). Resolves to a `ScriptRef` via the
1627        // registry. Precedence (Godot `reduce_identifier`): `class_name` global ≫ autoload
1628        // singleton. So try `class_name` first, then a `*`-autoload, then the seam.
1629        let by_class = resolve::resolve_external(
1630            self.db,
1631            &resolve::ExternalRef::ClassName(SmolStr::new(name)),
1632        );
1633        if !by_class.is_unknown() {
1634            return by_class;
1635        }
1636        resolve::resolve_external(self.db, &resolve::ExternalRef::Autoload(SmolStr::new(name)))
1637    }
1638
1639    fn own_member_ty(&self, item: ClassItem, as_method: bool) -> Ty {
1640        match item {
1641            ClassItem::EnumVariant => self.int_ty(),
1642            ClassItem::Member(_) => match self.class.member(item) {
1643                Some(Member::Var(v)) => self.field_ty(&v.name, v.ptr),
1644                Some(Member::Const(c)) => self.field_ty(&c.name, c.ptr),
1645                Some(Member::Func(f)) => {
1646                    if as_method {
1647                        self.func_return_ty(f.return_type.as_deref())
1648                    } else {
1649                        Ty::Callable
1650                    }
1651                }
1652                Some(Member::Signal(_)) => Ty::Signal(None),
1653                Some(Member::Class(_)) => Ty::Unknown,
1654                Some(Member::Enum(_)) | None => Ty::Variant,
1655            },
1656        }
1657    }
1658
1659    /// The type of an own field (`var`/`const`): the type seeded by the field pre-pass (which
1660    /// captures the inferred type of `var n := 0`), falling back to the written annotation.
1661    fn field_ty(&self, name: &str, ptr: AstPtr) -> Ty {
1662        if let Some(t) = self.class.member_types.get(name) {
1663            return t.clone();
1664        }
1665        self.resolve_decl_annotation(ptr)
1666    }
1667
1668    /// Resolve a declaration's annotation (recovering its `TypeRef` node), else `Variant`.
1669    fn resolve_decl_annotation(&self, ptr: AstPtr) -> Ty {
1670        let Some(node) = ptr.to_node(self.root) else {
1671            return Ty::Variant;
1672        };
1673        cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
1674            .map_or(Ty::Variant, |t| {
1675                resolve::resolve_type_ref(self.db, self.api, &t)
1676            })
1677    }
1678
1679    // ---- narrowing ----
1680
1681    /// Apply the narrowing implied by an `if`/`elif` condition to the current (cloned) branch.
1682    ///
1683    /// `is`-narrowing is a deliberate divergence from upstream Godot (whose `is` does **not**
1684    /// flow-narrow); we add it as a UX improvement but keep it **widen-only** so it never produces
1685    /// a type Godot's checker would reject: narrow only when the tested type is a strict downcast
1686    /// of the operand's current type, or the operand is uninformative. If the operand is already a
1687    /// subtype of the test (`d: Derived; if d is Base`), keep it — do not un-narrow. The
1688    /// `is_uninformative` guard also stays: never narrow to a type we couldn't resolve.
1689    fn apply_narrowing(&mut self, cond: ExprId) {
1690        let Expr::Is {
1691            operand,
1692            ty: Some(ptr),
1693            negated: false,
1694        } = self.body.expr(cond).clone()
1695        else {
1696            return;
1697        };
1698        let Some(key) = self.narrow_key(operand) else {
1699            return;
1700        };
1701        let narrowed = self.resolve_ptr_ty(ptr);
1702        if narrowed.is_uninformative() {
1703            return;
1704        }
1705        let cur = self.expr_ty.get(&operand).cloned().unwrap_or(Ty::Variant);
1706        if cur.is_uninformative() || self.is_subtype(&narrowed, &cur) {
1707            self.narrowing.insert(key, narrowed);
1708        }
1709    }
1710
1711    /// A dotted access-path key for narrowing (`x`, `self.field`, `a.b.c`), or `None` for a
1712    /// non-path expression.
1713    fn narrow_key(&self, id: ExprId) -> Option<String> {
1714        match self.body.expr(id) {
1715            Expr::Name(n) => Some(n.to_string()),
1716            Expr::SelfExpr => Some("self".to_owned()),
1717            Expr::Paren(inner) => self.narrow_key(*inner),
1718            Expr::Field { receiver, name, .. } => {
1719                Some(format!("{}.{name}", self.narrow_key(*receiver)?))
1720            }
1721            _ => None,
1722        }
1723    }
1724
1725    fn resolve_ptr_ty(&self, ptr: AstPtr) -> Ty {
1726        ptr.to_node(self.root).map_or(Ty::Variant, |n| {
1727            resolve::resolve_type_ref(self.db, self.api, &n)
1728        })
1729    }
1730
1731    // ---- helpers ----
1732
1733    /// The join (least upper bound) of two branch types — conservative: equal types collapse,
1734    /// a subtype widens to its supertype, else `Variant`.
1735    ///
1736    /// The three uninformative markers do NOT collapse to `Variant` — that would defeat the
1737    /// seam. They propagate by priority: `Error` (already diagnosed) → `Unknown` (the cross-file
1738    /// seam — must never warn or cascade) → `Variant` (the gradual top). So
1739    /// `x if c else <unknown>` stays `Unknown`, and `var y := (x if c else unknown)` does not
1740    /// fire a false `INFERENCE_ON_VARIANT`.
1741    fn join(&self, a: &Ty, b: &Ty) -> Ty {
1742        if a == b {
1743            return a.clone();
1744        }
1745        if a.is_error() || b.is_error() {
1746            return Ty::Error;
1747        }
1748        if a.is_unknown() || b.is_unknown() {
1749            return Ty::Unknown;
1750        }
1751        if a.is_variant() || b.is_variant() {
1752            return Ty::Variant;
1753        }
1754        if ty::is_assignable(self.api, a, b) == Assign::Ok {
1755            return b.clone();
1756        }
1757        if ty::is_assignable(self.api, b, a) == Assign::Ok {
1758            return a.clone();
1759        }
1760        Ty::Variant
1761    }
1762
1763    /// Run a closure within a branch-scoped narrowing frame (clone on enter, restore on exit).
1764    fn in_branch<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
1765        let saved = self.narrowing.clone();
1766        let r = f(self);
1767        self.narrowing = saved;
1768        r
1769    }
1770}
1771
1772/// Map a resolved global definition to the type of a bare reference to it.
1773fn global_ty(g: &GlobalDef) -> Ty {
1774    match g {
1775        GlobalDef::Const(t) => t.clone(),
1776        GlobalDef::Singleton(c) | GlobalDef::ClassType(c) => Ty::Object(*c),
1777        GlobalDef::BuiltinType(b) => Ty::Builtin(*b),
1778        // A bare function referenced as a value is a `Callable`; an enum namespace is opaque.
1779        GlobalDef::Builtin | GlobalDef::Utility => Ty::Callable,
1780        GlobalDef::GlobalEnum => Ty::Variant,
1781    }
1782}
1783
1784fn inference_on_variant_msg(kind: &str) -> String {
1785    format!(
1786        "The {kind} type is being inferred from a Variant value, so it will be typed as Variant."
1787    )
1788}
1789
1790/// The `extension_api.json` operator spelling for a binary operator.
1791fn op_symbol(op: BinOp) -> Option<&'static str> {
1792    Some(match op {
1793        BinOp::Add => "+",
1794        BinOp::Sub => "-",
1795        BinOp::Mul => "*",
1796        BinOp::Div => "/",
1797        BinOp::Mod => "%",
1798        BinOp::Pow => "**",
1799        BinOp::BitAnd => "&",
1800        BinOp::BitOr => "|",
1801        BinOp::BitXor => "^",
1802        BinOp::Shl => "<<",
1803        BinOp::Shr => ">>",
1804        _ => return None,
1805    })
1806}
1807
1808#[cfg(test)]
1809mod tests {
1810    use super::*;
1811    use crate::item_tree::item_tree;
1812    use gdscript_syntax::{SyntaxKind, parse};
1813
1814    struct Harness {
1815        result: InferenceResult,
1816        body: Body,
1817    }
1818
1819    /// Infer the (first) function in `src` against a fresh class scope.
1820    fn infer_first_func(src: &str) -> Harness {
1821        let api = gdscript_api::bundled();
1822        let db = gdscript_db::RootDatabase::default();
1823        let root = parse(src).syntax_node();
1824        let tree = item_tree(&root);
1825        let class = ClassScope::new(&db, api, &tree, None);
1826        let func = gdscript_syntax::ast::descendants(&root)
1827            .into_iter()
1828            .find(|n| n.kind() == SyntaxKind::FuncDecl)
1829            .expect("a function");
1830        let body = body::body_of_func(&func);
1831        let return_ty = cst::first_child(&func, |k| k == SyntaxKind::TypeRef)
1832            .map_or(Ty::Variant, |t| resolve::resolve_type_ref(&db, api, &t));
1833        let result = infer(&db, api, &root, &class, &body, return_ty);
1834        Harness { result, body }
1835    }
1836
1837    fn codes(h: &Harness) -> Vec<&str> {
1838        h.result
1839            .diagnostics
1840            .iter()
1841            .map(|d| d.code.as_str())
1842            .collect()
1843    }
1844
1845    /// Run the whole-file pass (Pass 1 field fixpoint + Pass 2 functions) and collect every
1846    /// diagnostic code. Drives `analyze_file` directly so the bounded member fixpoint runs.
1847    fn file_codes(src: &str) -> Vec<String> {
1848        let api = gdscript_api::bundled();
1849        let db = gdscript_db::RootDatabase::default();
1850        let root = parse(src).syntax_node();
1851        let fi = analyze_file(&db, api, &root, FileId(0));
1852        fi.diagnostics.iter().map(|d| d.code.clone()).collect()
1853    }
1854
1855    #[test]
1856    fn integer_division_warns() {
1857        let h = infer_first_func("func f():\n\tvar x = 5 / 2\n");
1858        assert!(codes(&h).contains(&INTEGER_DIVISION));
1859    }
1860
1861    #[test]
1862    fn float_div_does_not_warn() {
1863        let h = infer_first_func("func f():\n\tvar x = 5.0 / 2\n");
1864        assert!(!codes(&h).contains(&INTEGER_DIVISION));
1865    }
1866
1867    #[test]
1868    fn type_mismatch_on_hard_annotation() {
1869        let h = infer_first_func("func f():\n\tvar s: String = 5\n");
1870        assert!(codes(&h).contains(&TYPE_MISMATCH));
1871    }
1872
1873    #[test]
1874    fn narrowing_conversion_float_to_int() {
1875        let h = infer_first_func("func f():\n\tvar n: int = 1.5\n");
1876        assert!(codes(&h).contains(&NARROWING_CONVERSION));
1877    }
1878
1879    #[test]
1880    fn int_to_float_is_silent() {
1881        let h = infer_first_func("func f():\n\tvar x: float = 3\n");
1882        assert!(
1883            h.result.diagnostics.is_empty(),
1884            "{:?}",
1885            h.result.diagnostics
1886        );
1887    }
1888
1889    #[test]
1890    fn member_access_resolves_engine_property() {
1891        // In a Node script, bare `get_node(...)` resolves via the inherited base to Object(Node);
1892        // `get_parent()` is a real Node method → no UNSAFE.
1893        let h = infer_first_func(
1894            "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.get_parent()\n",
1895        );
1896        assert!(
1897            codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
1898            "{:?}",
1899            h.result.diagnostics
1900        );
1901    }
1902
1903    #[test]
1904    fn unsafe_method_on_known_type() {
1905        let h = infer_first_func(
1906            "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.totally_bogus_method()\n",
1907        );
1908        assert!(
1909            codes(&h).contains(&UNSAFE_METHOD_ACCESS),
1910            "{:?}",
1911            h.result.diagnostics
1912        );
1913    }
1914
1915    #[test]
1916    fn is_narrowing_suppresses_unsafe() {
1917        // Without narrowing, `x.free()` on an untyped param would be unchecked anyway; with
1918        // `is Node` it is checked against Node and `free` IS a Node method → no UNSAFE.
1919        let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.queue_free()\n");
1920        assert!(
1921            codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
1922            "{:?}",
1923            h.result.diagnostics
1924        );
1925    }
1926
1927    #[test]
1928    fn is_narrowing_flags_real_missing_member() {
1929        // After `is Node`, x is Node; `.bogus()` is genuinely missing → UNSAFE.
1930        let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.bogus_method()\n");
1931        assert!(codes(&h).contains(&UNSAFE_METHOD_ACCESS));
1932    }
1933
1934    #[test]
1935    fn variant_receiver_never_unsafe() {
1936        // Untyped param → Variant receiver → unchecked, no diagnostic.
1937        let h = infer_first_func("func f(x):\n\tx.anything_at_all()\n");
1938        assert!(
1939            h.result.diagnostics.is_empty(),
1940            "{:?}",
1941            h.result.diagnostics
1942        );
1943    }
1944
1945    #[test]
1946    fn unsafe_call_argument_on_variant_into_typed_param() {
1947        // Passing an untyped (Variant) value to a typed own-method parameter needs an unsafe cast.
1948        let h = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n: Node2D):\n\tpass\n");
1949        assert!(
1950            codes(&h).contains(&UNSAFE_CALL_ARGUMENT),
1951            "{:?}",
1952            h.result.diagnostics
1953        );
1954    }
1955
1956    #[test]
1957    fn unsafe_call_argument_silent_on_safe_and_untyped() {
1958        // A subtype arg (upcast) is safe; an untyped parameter accepts anything — neither warns.
1959        let upcast =
1960            infer_first_func("func f(n: Node2D):\n\ttake(n)\nfunc take(n: Node):\n\tpass\n");
1961        assert!(
1962            !codes(&upcast).contains(&UNSAFE_CALL_ARGUMENT),
1963            "upcast is safe: {:?}",
1964            upcast.result.diagnostics
1965        );
1966        let untyped = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n):\n\tpass\n");
1967        assert!(
1968            !codes(&untyped).contains(&UNSAFE_CALL_ARGUMENT),
1969            "untyped param accepts anything: {:?}",
1970            untyped.result.diagnostics
1971        );
1972    }
1973
1974    #[test]
1975    fn inference_on_variant() {
1976        // `:=` from an untyped (Variant) param.
1977        let h = infer_first_func("func f(x):\n\tvar y := x\n");
1978        assert!(codes(&h).contains(&INFERENCE_ON_VARIANT));
1979    }
1980
1981    #[test]
1982    fn field_inferred_from_earlier_field_is_typed() {
1983        // W2-MEMBER-FIXPOINT: `b`'s initializer references the earlier field `a`. A single shallow
1984        // field pass would see `a` as `Variant` (seam) and fire INFERENCE_ON_VARIANT on `:= a`; the
1985        // bounded fixpoint seeds `a: int` so `a + 1` is `int` and `:=` is precise — no warning.
1986        let codes = file_codes("var a := 1\nvar b := a + 1\n");
1987        assert!(
1988            !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
1989            "field `b` from earlier field `a` should type as int, not Variant: {codes:?}"
1990        );
1991    }
1992
1993    #[test]
1994    fn field_forward_reference_is_seamed_not_warned() {
1995        // A field referencing a *later* field still resolves through the fixpoint (both rounds
1996        // see each other's seeded type), and at worst lands on the conservative seam — never a
1997        // false INFERENCE_ON_VARIANT. (`b` precedes `a` lexically here.)
1998        let codes = file_codes("var b := a\nvar a := 1\n");
1999        assert!(
2000            !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
2001            "forward field reference must not false-warn: {codes:?}"
2002        );
2003    }
2004
2005    #[test]
2006    fn standalone_inferred_field_unchanged() {
2007        // No-regression: a self-contained inferred field still types from its literal, no warning.
2008        let codes = file_codes("var n := 0\n");
2009        assert!(
2010            codes.is_empty(),
2011            "a literal-initialised field should produce no diagnostics: {codes:?}"
2012        );
2013    }
2014
2015    #[test]
2016    fn lambda_var_is_callable_not_variant() {
2017        let h = infer_first_func("func f():\n\tvar cb := func():\n\t\tpass\n");
2018        assert!(
2019            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2020            "{:?}",
2021            h.result.diagnostics
2022        );
2023    }
2024
2025    #[test]
2026    fn multiline_lambda_then_paren_line_no_false_warning() {
2027        // A multi-line lambda bound to a var, followed by a statement that begins with `(`.
2028        // The parser now ends the lambda at its dedent (the `(` line is its own statement), so
2029        // there is no spurious call-on-lambda and no false `INFERENCE_ON_VARIANT`.
2030        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";
2031        let h = infer_first_func(src);
2032        assert!(
2033            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2034            "{:?}",
2035            h.result.diagnostics
2036        );
2037    }
2038
2039    #[test]
2040    fn calling_a_callable_value_is_seam_not_variant() {
2041        // Invoking an arbitrary expression (here a parenthesized `Callable` value) reaches the
2042        // seam arm of `infer_call`: the return type isn't tracked, so the result is Unknown,
2043        // not `Variant`, and the inferred-on-Variant warning never fires.
2044        let src = "func f(cb: Callable):\n\tvar x := (cb)()\n\treturn x\n";
2045        let h = infer_first_func(src);
2046        assert!(
2047            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2048            "{:?}",
2049            h.result.diagnostics
2050        );
2051    }
2052
2053    #[test]
2054    fn ternary_with_seam_branch_does_not_collapse_to_variant() {
2055        // A ternary whose else-branch is the seam (`await` is untracked → Unknown) must `join`
2056        // to Unknown, NOT Variant — otherwise `var x := …` fires a false INFERENCE_ON_VARIANT.
2057        // (Regression: `join` used to absorb any uninformative branch to Variant.)
2058        let src =
2059            "func f(c: bool):\n\tvar x := 5 if c else await get_tree().process_frame\n\treturn x\n";
2060        let h = infer_first_func(src);
2061        assert!(
2062            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2063            "seam branch should keep the ternary on the seam: {:?}",
2064            h.result.diagnostics
2065        );
2066    }
2067
2068    #[test]
2069    fn await_a_coroutine_call_recovers_its_return_type() {
2070        // `await f()` yields the call's value, so await is identity on a non-signal operand:
2071        // `await make()` for `func make() -> int` types `x` as int (was the seam before).
2072        let src = "func g() -> int:\n\tvar x := await make()\n\treturn x\nfunc make() -> int:\n\treturn 5\n";
2073        let h = infer_first_func(src);
2074        assert!(
2075            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2076            "no false variant warning: {:?}",
2077            h.result.diagnostics
2078        );
2079        let api = gdscript_api::bundled();
2080        let x = &h.result.bindings[0];
2081        assert!(
2082            matches!(&x.ty, Ty::Builtin(b) if api.builtin(*b).name == "int"),
2083            "await make() should recover int, got {:?}",
2084            x.ty
2085        );
2086    }
2087
2088    #[test]
2089    fn await_a_signal_stays_the_seam() {
2090        // `await sig` yields the signal's payload (needs the Phase-3+ sig table) — must stay the seam,
2091        // never the Signal type itself, and never a false INFERENCE_ON_VARIANT.
2092        let src = "func f():\n\tvar x := await get_tree().process_frame\n\treturn x\n";
2093        let h = infer_first_func(src);
2094        assert!(
2095            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2096            "awaiting a signal must not warn: {:?}",
2097            h.result.diagnostics
2098        );
2099        assert!(
2100            matches!(&h.result.bindings[0].ty, Ty::Unknown),
2101            "awaiting a signal stays the seam, got {:?}",
2102            h.result.bindings[0].ty
2103        );
2104    }
2105
2106    #[test]
2107    fn for_var_over_packed_string_array_is_string() {
2108        // `for s in "a,b".split(",")` iterates a PackedStringArray → String, so `s.to_int()`
2109        // resolves and `var x := s` does not warn.
2110        let h = infer_first_func("func f():\n\tfor s in \"a,b\".split(\",\"):\n\t\tvar x := s\n");
2111        assert!(
2112            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2113            "{:?}",
2114            h.result.diagnostics
2115        );
2116    }
2117
2118    #[test]
2119    fn class_new_is_object_not_variant() {
2120        let h = infer_first_func("func f():\n\tvar s := GDScript.new()\n");
2121        assert!(
2122            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2123            "{:?}",
2124            h.result.diagnostics
2125        );
2126    }
2127
2128    #[test]
2129    fn unknown_seam_never_warns() {
2130        // `preload(...)` is Unknown; `:=` from it does NOT warn, and member access is unchecked.
2131        let h = infer_first_func("func f():\n\tvar s := preload(\"res://x.gd\")\n\ts.whatever()\n");
2132        assert!(
2133            h.result.diagnostics.is_empty(),
2134            "{:?}",
2135            h.result.diagnostics
2136        );
2137    }
2138
2139    #[test]
2140    fn expr_types_are_memoized_for_hover() {
2141        let h = infer_first_func("func f():\n\tvar n := 42\n");
2142        // The `42` literal expr should be typed int.
2143        let has_int = h
2144            .result
2145            .expr_ty
2146            .values()
2147            .any(|t| matches!(t, Ty::Builtin(_)));
2148        assert!(has_int);
2149        // sanity: the body lowered at least one expr
2150        assert!(!h.body.exprs.is_empty());
2151    }
2152}