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            // The path descends into an instanced sub-scene (`$Enemy/Sprite`): resolve the tail in
942            // the sub-scene's own tree (`Sprite` typed by `enemy.tscn`). Any failure → `Node`.
943            R::IntoInstance => {
944                let walked = if unique {
945                    ctx.model.resolve_unique_into_instance(path)
946                } else {
947                    ctx.model.resolve_into_instance(ctx.attach, path)
948                };
949                walked
950                    .and_then(|(inst, tail)| {
951                        let inst_node = ctx.model.node(inst)?;
952                        self.resolve_into_instance_ty(&ctx.model, inst_node, &tail, 0)
953                    })
954                    .unwrap_or(fallback)
955            }
956            // ambiguous miss / escape (`..`/absolute) → `Node`, never a false warning
957            _ => fallback,
958        }
959    }
960
961    /// The owning-scene context for the current file (scene + attach node + multi-scene ambiguity).
962    /// Recovered from `self_ty`, which `analyze_file` sets to the file's own `ScriptRef` (so no extra
963    /// `FileId` threading).
964    fn owning_scene(&self) -> Option<crate::queries::SceneContext> {
965        let Ty::ScriptRef(sref) = &self.self_ty else {
966            return None;
967        };
968        let ft = self.db.file_text(FileId(sref.0))?;
969        crate::queries::scene_context(self.db, ft)
970    }
971
972    /// The importing file's own `res://` path (from `self_ty`), for anchoring relative
973    /// `preload`/`extends` paths to its directory. `None` when the file has no resource path.
974    fn self_res_path(&self) -> Option<SmolStr> {
975        let Ty::ScriptRef(sref) = &self.self_ty else {
976            return None;
977        };
978        self.db.file_text(FileId(sref.0))?.res_path(self.db)
979    }
980
981    /// The concrete `Ty` of a scene node, by precedence: an attached script's own class (most
982    /// specific) wins; else the declared `type=` (native class or `class_name`); else — an instanced
983    /// node (`instance=`, no own `type=`/script) — the **instanced sub-scene's root** type (M3,
984    /// recursive). `None` for a node we can't sharpen (the caller degrades to `Node`).
985    fn scene_node_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
986        if let Some(script_ty) = self.node_script_ref(scene, node) {
987            return Some(script_ty);
988        }
989        if let Some(decl) = node.decl_type.as_ref() {
990            let ty = resolve::resolve_type_name(self.db, self.api, decl);
991            if !ty.is_uninformative() {
992                return Some(ty);
993            }
994        }
995        self.instance_root_ty(scene, node, depth)
996    }
997
998    /// An instanced node (`instance=ExtResource(id)`) takes the type of the instanced sub-scene's
999    /// ROOT node — resolved recursively, so the root's own script / `type=` / nested instance all
1000    /// flow through (so `$Enemy` types as `enemy.tscn`'s root class, not bare `Node`). Depth-bounded
1001    /// against an instancing cycle (scene A instances B instances A).
1002    fn instance_root_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
1003        if depth >= 16 {
1004            return None;
1005        }
1006        let (sub, sub_root) = self.instance_subscene(scene, node)?;
1007        let root_node = sub.node(sub_root)?;
1008        self.scene_node_ty(&sub, root_node, depth + 1)
1009    }
1010
1011    /// The instanced sub-scene's model + its root index, for an instance node (`instance=ExtResource`
1012    /// → `res://` path → `FileId` → `scene_model`). The shared resolution step for both
1013    /// [`instance_root_ty`](Self::instance_root_ty) (the node's own type) and
1014    /// [`resolve_into_instance_ty`](Self::resolve_into_instance_ty) (paths that go *into* it).
1015    fn instance_subscene(
1016        &self,
1017        scene: &SceneModel,
1018        node: &SceneNode,
1019    ) -> Option<(Arc<SceneModel>, gdscript_scene::NodeIdx)> {
1020        let inst = node.instance.as_ref()?;
1021        let path = scene.ext_resources.get(inst)?.path.as_ref()?;
1022        let root = self.db.source_root()?;
1023        let file = crate::queries::res_path_registry(self.db, root)
1024            .get(path.as_str())
1025            .copied()?;
1026        let ft = self.db.file_text(file)?;
1027        let sub = crate::queries::scene_model(self.db, ft);
1028        let sub_root = sub.root?;
1029        Some((sub, sub_root))
1030    }
1031
1032    /// Type a node path that descends INTO an instanced sub-scene: `instance_node` is the boundary
1033    /// (an `instance=` node) and `tail` is the remaining path. Resolve `tail` from the sub-scene's
1034    /// root, recursing through further instance boundaries inside it. Depth-bounded against an
1035    /// instancing cycle. `None` (→ `Node`, no false warning) if the tail genuinely can't be typed.
1036    fn resolve_into_instance_ty(
1037        &self,
1038        scene: &SceneModel,
1039        instance_node: &SceneNode,
1040        tail: &str,
1041        depth: u32,
1042    ) -> Option<Ty> {
1043        if depth >= 16 {
1044            return None;
1045        }
1046        let (sub, sub_root) = self.instance_subscene(scene, instance_node)?;
1047        if let Some(idx) = sub.resolve_path_from(sub_root, tail) {
1048            let n = sub.node(idx)?;
1049            return self.scene_node_ty(&sub, n, depth + 1);
1050        }
1051        // The tail crosses a further instance boundary *inside* the sub-scene — keep descending.
1052        let (inner, inner_tail) = sub.resolve_into_instance(sub_root, tail)?;
1053        let inner_node = sub.node(inner)?;
1054        self.resolve_into_instance_ty(&sub, inner_node, &inner_tail, depth + 1)
1055    }
1056
1057    /// The `ScriptRef` of a node's attached `.gd` script (`script = ExtResource(id)` → its `res://`
1058    /// path → `FileId`), or `None` if it has no resolvable external script.
1059    fn node_script_ref(&self, scene: &SceneModel, node: &SceneNode) -> Option<Ty> {
1060        let path = scene
1061            .ext_resources
1062            .get(node.script.as_ref()?)?
1063            .path
1064            .as_ref()?;
1065        let root = self.db.source_root()?;
1066        let file = crate::queries::res_path_registry(self.db, root)
1067            .get(path.as_str())
1068            .copied()?;
1069        Some(Ty::ScriptRef(ScriptRefId(file.0)))
1070    }
1071
1072    fn infer_bin(&mut self, id: ExprId, op: BinOp, lhs: ExprId, rhs: ExprId) -> Ty {
1073        if op == BinOp::Assign {
1074            return self.infer_assign(lhs, rhs);
1075        }
1076        let lt = self.infer_expr(lhs, &Expectation::None);
1077        let rt = self.infer_expr(rhs, &Expectation::None);
1078        if op.is_boolean() {
1079            return self.bool_ty();
1080        }
1081        // `int / int` discards the fractional part.
1082        if op == BinOp::Div && self.is_int(&lt) && self.is_int(&rt) {
1083            self.emit(
1084                self.range_of(id),
1085                Severity::Warning,
1086                INTEGER_DIVISION,
1087                "Integer division. Decimal part will be discarded.".to_owned(),
1088            );
1089            return self.int_ty();
1090        }
1091        self.bin_result(op, &lt, &rt)
1092    }
1093
1094    fn infer_assign(&mut self, lhs: ExprId, rhs: ExprId) -> Ty {
1095        let slot = self.infer_expr(lhs, &Expectation::None);
1096        let expected = if slot.is_uninformative() {
1097            Expectation::None
1098        } else {
1099            Expectation::Has(slot.clone())
1100        };
1101        let value = self.infer_expr(rhs, &expected);
1102        if !slot.is_uninformative() {
1103            self.check_assign(&value, &slot, self.range_of(rhs));
1104        }
1105        // Assignment narrowing: bound the narrowed type by the declared slot.
1106        if let Some(key) = self.narrow_key(lhs) {
1107            let narrowed = if slot.is_uninformative() {
1108                value.clone()
1109            } else {
1110                slot.clone()
1111            };
1112            self.narrowing.insert(key, narrowed);
1113        }
1114        slot
1115    }
1116
1117    /// Resolve a binary operator's result type via the builtin operator table, with a numeric
1118    /// fallback. Comparison/logical operators are handled by the caller.
1119    fn bin_result(&self, op: BinOp, lt: &Ty, rt: &Ty) -> Ty {
1120        if let (Ty::Builtin(b), Some(sym)) = (lt, op_symbol(op)) {
1121            for o in self.api.builtin_operators(*b) {
1122                if o.op == sym
1123                    && let Some(right) = &o.right
1124                    && self.tyref_matches(right, rt)
1125                {
1126                    return ty::resolve_tyref(self.api, &o.result);
1127                }
1128            }
1129        }
1130        if self.is_numeric(lt) && self.is_numeric(rt) {
1131            return if self.is_float(lt) || self.is_float(rt) {
1132                self.float_ty()
1133            } else {
1134                self.int_ty()
1135            };
1136        }
1137        // A seam operand keeps the result on the seam (`a + unknown` is `Unknown`, not the
1138        // gradual `Variant`, so `var x := a + unknown` never warns).
1139        if lt.is_unknown() || rt.is_unknown() || lt.is_error() || rt.is_error() {
1140            return Ty::Unknown;
1141        }
1142        Ty::Variant
1143    }
1144
1145    fn tyref_matches(&self, tyref: &TyRef, ty: &Ty) -> bool {
1146        let resolved = ty::resolve_tyref(self.api, tyref);
1147        resolved.is_variant() || &resolved == ty
1148    }
1149
1150    fn infer_call(&mut self, callee: ExprId, args: &[ExprId]) -> Ty {
1151        // Argument expressions are always inferred (their own diagnostics + hover).
1152        for &a in args {
1153            self.infer_expr(a, &Expectation::None);
1154        }
1155        let ret = match self.body.expr(callee).clone() {
1156            Expr::Field {
1157                receiver,
1158                name,
1159                name_range,
1160            } => {
1161                self.infer_field(receiver, &name, name_range, /*as_method=*/ true)
1162            }
1163            Expr::Name(name) => {
1164                let ret = self.resolve_call_name(&name);
1165                self.expr_ty.insert(callee, Ty::Callable);
1166                ret
1167            }
1168            // Calling an arbitrary expression — a `Callable` value or an immediately-invoked
1169            // lambda (`(func(): …).call()`): the callee's return type isn't tracked, so the
1170            // result is the seam (not `Variant`), and `var x := f()()` never warns.
1171            _ => {
1172                self.infer_expr(callee, &Expectation::None);
1173                Ty::Unknown
1174            }
1175        };
1176        // UNSAFE_CALL_ARGUMENT (Phase-2 §5): args + receiver are now inferred (in `expr_ty`), so
1177        // check each argument against the statically-resolved callee's parameter types.
1178        self.check_call_args(callee, args);
1179        ret
1180    }
1181
1182    /// Raise `UNSAFE_CALL_ARGUMENT` for each argument whose static type needs an unsafe implicit
1183    /// cast (`Variant` / a downcast) into the resolved parameter type — Godot's per-argument
1184    /// value-prop warning. Only fires when the callee resolves to a concrete signature here; an
1185    /// uninformative argument (the cross-file seam) is `Assign::Ok` and correctly silent, and an
1186    /// untyped parameter accepts anything.
1187    fn check_call_args(&mut self, callee: ExprId, args: &[ExprId]) {
1188        let Some(params) = self.call_param_tys(callee) else {
1189            return;
1190        };
1191        for (i, &arg) in args.iter().enumerate() {
1192            let Some(param_ty) = params.get(i) else {
1193                break; // a vararg tail or an arity mismatch — not an argument-type concern
1194            };
1195            if param_ty.is_uninformative() || param_ty.is_variant() {
1196                continue; // an untyped parameter accepts anything safely
1197            }
1198            // A missing arg type defaults to the seam (never warns), not `Variant` (would warn).
1199            let arg_ty = self.expr_ty.get(&arg).cloned().unwrap_or(Ty::Unknown);
1200            if ty::is_assignable(self.api, &arg_ty, param_ty) == Assign::OkUnsafe {
1201                let pl = param_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
1202                let al = arg_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
1203                self.emit(
1204                    self.range_of(arg),
1205                    Severity::Warning,
1206                    UNSAFE_CALL_ARGUMENT,
1207                    format!(
1208                        "The argument {} requires a value of type \"{pl}\" but is passed \"{al}\", which is unsafe.",
1209                        i + 1
1210                    ),
1211                );
1212            }
1213        }
1214    }
1215
1216    /// Parameter types of a statically-resolved callee, for [`Self::check_call_args`]. `None` when
1217    /// the callee isn't concretely resolvable here (a cross-file script method — params aren't
1218    /// modeled —, a builtin/utility, a `Callable` value): those raise no argument warning.
1219    fn call_param_tys(&self, callee: ExprId) -> Option<Vec<Ty>> {
1220        match self.body.expr(callee) {
1221            Expr::Name(name) => self.name_call_param_tys(name),
1222            Expr::Field { receiver, name, .. } => match self.expr_ty.get(receiver)? {
1223                Ty::Object(class) => match self.api.lookup_member(*class, name)? {
1224                    MemberRef::Method(sig) => Some(
1225                        sig.params
1226                            .iter()
1227                            .map(|p| ty::resolve_tyref(self.api, &p.ty))
1228                            .collect(),
1229                    ),
1230                    _ => None,
1231                },
1232                // ScriptRef / builtin / seam receivers: params not uniformly modeled — skip.
1233                _ => None,
1234            },
1235            _ => None,
1236        }
1237    }
1238
1239    /// Parameter types for a bare-name call (`foo(...)` / an inherited `method(...)`): an own `func`
1240    /// first, then the `self` engine base's method. Utilities/builtins are skipped (looser, often
1241    /// variadic typing — out of the conservative MVP slice).
1242    fn name_call_param_tys(&self, name: &str) -> Option<Vec<Ty>> {
1243        if let Some(item) = self.class.lookup(name)
1244            && let Some(Member::Func(f)) = self.class.member(item)
1245        {
1246            return Some(
1247                f.params
1248                    .iter()
1249                    .map(|p| {
1250                        p.type_ref.as_deref().map_or(Ty::Variant, |t| {
1251                            resolve::resolve_type_name(self.db, self.api, t)
1252                        })
1253                    })
1254                    .collect(),
1255            );
1256        }
1257        if let Ty::Object(base) = self.class.base
1258            && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
1259        {
1260            return Some(
1261                sig.params
1262                    .iter()
1263                    .map(|p| ty::resolve_tyref(self.api, &p.ty))
1264                    .collect(),
1265            );
1266        }
1267        None
1268    }
1269
1270    /// Resolve a bare-name call (`foo(...)`): own method → utility/builtin fn → constructor.
1271    fn resolve_call_name(&self, name: &str) -> Ty {
1272        if let Some(item) = self.class.lookup(name)
1273            && let Some(Member::Func(f)) = self.class.member(item)
1274        {
1275            return self.func_return_ty(f.return_type.as_deref());
1276        }
1277        // A bare call inside the class is `self.name(...)` — resolve against the inherited base.
1278        if let Ty::Object(base) = self.class.base
1279            && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
1280        {
1281            return ty::resolve_tyref(self.api, &sig.return_ty);
1282        }
1283        if let Some(u) = self.api.utility(name) {
1284            return ty::resolve_tyref(self.api, &u.return_ty);
1285        }
1286        if let Some(f) = self.api.gdscript_builtin(name) {
1287            return resolve::layer_to_ty(self.api, f.ret);
1288        }
1289        // A builtin / class name used as a constructor: `Vector2(...)` / `Array(...)`.
1290        // Normalize via `resolve_tyref` so `Array`/`Dictionary`/`Callable`/`Signal` land on
1291        // their dedicated `Ty` variants rather than `Builtin(...)`.
1292        if let Some(b) = self.api.builtin_by_name(name) {
1293            return ty::resolve_tyref(self.api, &TyRef::Builtin(b));
1294        }
1295        // Otherwise unresolved — most likely a cross-file global / autoload / a method on a
1296        // `class_name` base we can't see. Treat as the seam so `var x := foo()` never warns.
1297        Ty::Unknown
1298    }
1299
1300    fn func_return_ty(&self, annotation: Option<&str>) -> Ty {
1301        annotation.map_or(Ty::Variant, |t| {
1302            resolve::resolve_type_name(self.db, self.api, t)
1303        })
1304    }
1305
1306    /// Member access `receiver.name`. When `as_method`, resolve a method (and use its return
1307    /// type); otherwise resolve a property/const/etc. Raises `UNSAFE_*` only on a statically
1308    /// **known** receiver.
1309    fn infer_field(
1310        &mut self,
1311        receiver: ExprId,
1312        name: &str,
1313        name_range: TextRange,
1314        as_method: bool,
1315    ) -> Ty {
1316        let is_self = matches!(self.body.expr(receiver), Expr::SelfExpr);
1317        let recv_ty = self.infer_expr(receiver, &Expectation::None);
1318
1319        // `self.member` consults this file's own members first (Playbook §3.2).
1320        if is_self && let Some(item) = self.class.lookup(name) {
1321            return self.own_member_ty(item, as_method);
1322        }
1323
1324        match &recv_ty {
1325            // Uninformative receivers are unchecked and **propagate the seam**: a member of an
1326            // `Unknown` (cross-file) value is itself `Unknown` (never warns), a member of a
1327            // `Variant` is `Variant`, of an `Error` is `Error`. Collapsing `Unknown` to
1328            // `Variant` here would wrongly fire `INFERENCE_ON_VARIANT` on `var x := other.field`.
1329            t if t.is_uninformative() => recv_ty.clone(),
1330            Ty::Object(class) => {
1331                if name == "new" {
1332                    // `Class.new(...)` always constructs an instance of the class (some classes,
1333                    // e.g. GDScript, also carry a modeled `new` member — the constructor wins).
1334                    recv_ty.clone()
1335                } else if let Some(m) = self.api.lookup_member(*class, name) {
1336                    self.member_ref_ty(&m, as_method)
1337                } else if let Some(t) = self.class_enum_value(*class, name) {
1338                    // A statically-accessed enum value (`Control.PRESET_FULL_RECT`).
1339                    t
1340                } else {
1341                    // Self with an Object base already checked own members above.
1342                    self.emit_unsafe(name, &recv_ty, name_range, as_method);
1343                    Ty::Variant
1344                }
1345            }
1346            Ty::Builtin(_) | Ty::Array(_) | Ty::Dict(..) | Ty::Callable | Ty::Signal(_) => {
1347                self.builtin_member_ty(&recv_ty, name, name_range, as_method)
1348            }
1349            // Enum value access (`MyEnum.VALUE`) is an `int`.
1350            Ty::Enum(_) => self.int_ty(),
1351            // A cross-file script reference: resolve the member against its (own) member table.
1352            Ty::ScriptRef(sref) => self.script_member_ty(*sref, name, as_method),
1353            _ => Ty::Variant,
1354        }
1355    }
1356
1357    /// A member of a cross-file script (`ScriptRef`): looked up in the script's own member table
1358    /// (M1). A member we don't model — e.g. one inherited from a base we don't resolve until M2 —
1359    /// yields the seam (`Unknown`), **never** an `UNSAFE_*` warning. `Class.new(...)` constructs
1360    /// an instance of the class.
1361    fn script_member_ty(&self, sref: ScriptRefId, name: &str, as_method: bool) -> Ty {
1362        if name == "new" {
1363            return Ty::ScriptRef(sref);
1364        }
1365        self.script_member_walk(sref, name, as_method, 0)
1366            .unwrap_or(Ty::Unknown)
1367    }
1368
1369    /// Walk a script class's `extends` chain for `name`: own members first, then a user base
1370    /// (another `ScriptRef`), then an engine base (the API table). Depth-bounded so a cyclic
1371    /// `extends` cannot loop. `None` = not found anywhere in the chain (the seam).
1372    fn script_member_walk(
1373        &self,
1374        sref: ScriptRefId,
1375        name: &str,
1376        as_method: bool,
1377        depth: u32,
1378    ) -> Option<Ty> {
1379        if depth > 32 {
1380            return None;
1381        }
1382        let file = self.db.file_text(FileId(sref.0))?;
1383        let sc = crate::queries::script_class(self.db, file);
1384        if let Some(m) = sc.member(name) {
1385            return Some(match m {
1386                crate::queries::MemberSig::Method(ret) => {
1387                    if as_method {
1388                        ret.clone()
1389                    } else {
1390                        Ty::Callable
1391                    }
1392                }
1393                crate::queries::MemberSig::Field(t) => t.clone(),
1394                crate::queries::MemberSig::Signal => Ty::Signal(None),
1395            });
1396        }
1397        // Not an own member — continue up the inheritance chain.
1398        match sc.base() {
1399            Ty::ScriptRef(base) => self.script_member_walk(*base, name, as_method, depth + 1),
1400            Ty::Object(class) => self
1401                .api
1402                .lookup_member(*class, name)
1403                .map(|m| self.member_ref_ty(&m, as_method)),
1404            _ => None,
1405        }
1406    }
1407
1408    /// Whether a value of type `sub` is statically a subtype of `sup` — composing user `ScriptRef`
1409    /// `extends` chains with the engine class table (M4, for `is`/`as` widen-only narrowing). A
1410    /// `ScriptRef` IS-A its native base (so `script_value is Node` holds), but Godot's asymmetry is
1411    /// honored: a native/script value is **not** a subtype of an *unrelated* user script.
1412    fn is_subtype(&self, sub: &Ty, sup: &Ty) -> bool {
1413        match (sub, sup) {
1414            (Ty::Object(a), Ty::Object(b)) => self.api.is_subclass(*a, *b),
1415            (Ty::ScriptRef(a), Ty::ScriptRef(b)) => self.script_is_subtype(*a, *b, 0),
1416            (Ty::ScriptRef(a), Ty::Object(b)) => self.script_extends_engine(*a, *b, 0),
1417            _ => false,
1418        }
1419    }
1420
1421    /// Whether script `sub` is `sup` or transitively extends it — walk the `extends` base chain by
1422    /// script identity (depth-bounded, like [`script_member_walk`](Self::script_member_walk)).
1423    fn script_is_subtype(&self, sub: ScriptRefId, sup: ScriptRefId, depth: u32) -> bool {
1424        if depth > 32 {
1425            return false;
1426        }
1427        if sub == sup {
1428            return true;
1429        }
1430        let Some(file) = self.db.file_text(FileId(sub.0)) else {
1431            return false;
1432        };
1433        match crate::queries::script_class(self.db, file).base() {
1434            Ty::ScriptRef(base) => self.script_is_subtype(*base, sup, depth + 1),
1435            _ => false,
1436        }
1437    }
1438
1439    /// Whether script `sub`'s `extends` chain reaches engine class `sup_native` at its native base.
1440    fn script_extends_engine(
1441        &self,
1442        sub: ScriptRefId,
1443        sup_native: gdscript_api::ClassId,
1444        depth: u32,
1445    ) -> bool {
1446        if depth > 32 {
1447            return false;
1448        }
1449        let Some(file) = self.db.file_text(FileId(sub.0)) else {
1450            return false;
1451        };
1452        match crate::queries::script_class(self.db, file).base() {
1453            Ty::ScriptRef(base) => self.script_extends_engine(*base, sup_native, depth + 1),
1454            Ty::Object(native) => self.api.is_subclass(*native, sup_native),
1455            _ => false,
1456        }
1457    }
1458
1459    fn emit_unsafe(&mut self, name: &str, recv: &Ty, range: TextRange, as_method: bool) {
1460        let recv_label = recv.label(self.api).unwrap_or_else(|| "?".to_owned());
1461        let (code, message) = if as_method {
1462            (
1463                UNSAFE_METHOD_ACCESS,
1464                format!(
1465                    "The method \"{name}()\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1466                ),
1467            )
1468        } else {
1469            (
1470                UNSAFE_PROPERTY_ACCESS,
1471                format!(
1472                    "The property \"{name}\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1473                ),
1474            )
1475        };
1476        self.emit(range, Severity::Warning, code, message);
1477    }
1478
1479    fn member_ref_ty(&self, m: &MemberRef, as_method: bool) -> Ty {
1480        match m {
1481            MemberRef::Method(sig) => {
1482                if as_method {
1483                    ty::resolve_tyref(self.api, &sig.return_ty)
1484                } else {
1485                    Ty::Callable
1486                }
1487            }
1488            MemberRef::Property(p) => p.enum_of.as_ref().map_or_else(
1489                || ty::resolve_tyref(self.api, &p.ty),
1490                |q| {
1491                    Ty::Enum(EnumRef {
1492                        qualified: SmolStr::new(q),
1493                        bitfield: false,
1494                    })
1495                },
1496            ),
1497            MemberRef::Const(c) => ty::resolve_tyref(self.api, &c.ty),
1498            MemberRef::Signal(_) => Ty::Signal(None),
1499            MemberRef::Enum(_) => Ty::Variant,
1500        }
1501    }
1502
1503    fn builtin_member_ty(
1504        &mut self,
1505        recv: &Ty,
1506        name: &str,
1507        range: TextRange,
1508        as_method: bool,
1509    ) -> Ty {
1510        let Some(bid) = self.builtin_id_of(recv) else {
1511            return Ty::Variant;
1512        };
1513        if as_method {
1514            return if let Some(sig) = self.api.builtin_method(bid, name) {
1515                ty::resolve_tyref(self.api, &sig.return_ty)
1516            } else {
1517                self.emit_unsafe(name, recv, range, true);
1518                Ty::Variant
1519            };
1520        }
1521        if let Some(member) = self.api.builtin_member(bid, name) {
1522            return ty::resolve_tyref(self.api, &member.ty);
1523        }
1524        // Static constants (`Vector2.ZERO`, `Color.WHITE`) and enum values (`Variant.Type.*`).
1525        let data = self.api.builtin(bid);
1526        if let Some(c) = data.constants.iter().find(|c| c.name == name) {
1527            return ty::resolve_tyref(self.api, &c.ty);
1528        }
1529        if data
1530            .enums
1531            .iter()
1532            .any(|e| e.values.iter().any(|v| v.name == name))
1533        {
1534            return self.int_ty();
1535        }
1536        if self.api.builtin_method(bid, name).is_some() {
1537            return Ty::Callable;
1538        }
1539        self.emit_unsafe(name, recv, range, false);
1540        Ty::Variant
1541    }
1542
1543    /// The type of a class enum **value** accessed statically (`Control.PRESET_FULL_RECT`):
1544    /// the engine exposes enum values as class members, so search every (inherited) enum's
1545    /// values. Returns `int` when found.
1546    fn class_enum_value(&self, class: gdscript_api::ClassId, name: &str) -> Option<Ty> {
1547        let mut cur = Some(class);
1548        while let Some(cid) = cur {
1549            let c = self.api.class(cid);
1550            if c.enums
1551                .iter()
1552                .any(|e| e.values.iter().any(|v| v.name == name))
1553            {
1554                return Some(self.int_ty());
1555            }
1556            cur = c.base;
1557        }
1558        None
1559    }
1560
1561    /// The builtin id backing a builtin / `Array` / `Dictionary` receiver.
1562    fn builtin_id_of(&self, ty: &Ty) -> Option<gdscript_api::BuiltinId> {
1563        match ty {
1564            Ty::Builtin(b) => Some(*b),
1565            Ty::Array(_) => self.api.builtin_by_name("Array"),
1566            Ty::Dict(..) => self.api.builtin_by_name("Dictionary"),
1567            Ty::Callable => self.api.builtin_by_name("Callable"),
1568            Ty::Signal(_) => self.api.builtin_by_name("Signal"),
1569            _ => None,
1570        }
1571    }
1572
1573    /// The element type of an indexing expression (Playbook §2 switch).
1574    fn index_ty(&self, base: &Ty) -> Ty {
1575        match base {
1576            Ty::Array(elem) => (**elem).clone(),
1577            Ty::Builtin(b) => self
1578                .api
1579                .builtin(*b)
1580                .indexing_return
1581                .as_ref()
1582                .map_or(Ty::Variant, |r| ty::resolve_tyref(self.api, r)),
1583            // Indexing through the seam stays on the seam (never warns).
1584            Ty::Unknown => Ty::Unknown,
1585            Ty::Error => Ty::Error,
1586            _ => Ty::Variant,
1587        }
1588    }
1589
1590    /// The loop variable's type for `for v in iter:` (Playbook §2 switch).
1591    fn loop_var_ty(&self, iter: &Ty) -> Ty {
1592        match iter {
1593            Ty::Array(elem) => (**elem).clone(),
1594            Ty::Builtin(b) => {
1595                let data = self.api.builtin(*b);
1596                if data.name == "int" {
1597                    // `for i in 5` / `for i in range(...)` → int.
1598                    self.int_ty()
1599                } else if let Some(r) = &data.indexing_return {
1600                    // `for c in "abc"` → String; `for s in packed_string_array` → String; …
1601                    ty::resolve_tyref(self.api, r)
1602                } else {
1603                    Ty::Variant
1604                }
1605            }
1606            // Iterating a seam value keeps the loop var on the seam (never warns).
1607            Ty::Unknown => Ty::Unknown,
1608            Ty::Error => Ty::Error,
1609            _ => Ty::Variant,
1610        }
1611    }
1612
1613    fn infer_lambda(&mut self, params: &[ParamBinding], body: &[body::StmtId]) {
1614        // Lambda params shadow within the body; restore the outer locals afterward. A `return`
1615        // inside the lambda is the *lambda's* return, not the enclosing function's — so disable
1616        // return checking (set the expected return to `Variant`) while walking the body.
1617        let saved_locals = self.locals.clone();
1618        let saved_ret = std::mem::replace(&mut self.return_ty, Ty::Variant);
1619        for p in params {
1620            let ty = self.param_ty(p);
1621            self.bindings.push(Binding {
1622                name_range: p.name_range,
1623                ty: ty.clone(),
1624                init: None,
1625                annotated: p.type_ref.is_some(),
1626                inferred_colon_eq: false,
1627                kind: BindingKind::Param,
1628            });
1629            self.locals.insert(p.name.clone(), ty);
1630        }
1631        self.infer_block(body);
1632        self.return_ty = saved_ret;
1633        self.locals = saved_locals;
1634    }
1635
1636    fn param_ty(&mut self, p: &ParamBinding) -> Ty {
1637        if let Some(ptr) = p.type_ref {
1638            return self.resolve_ptr_ty(ptr);
1639        }
1640        // An unannotated param infers from its default, else `Variant`.
1641        p.default
1642            .map_or(Ty::Variant, |e| self.infer_expr(e, &Expectation::None))
1643    }
1644
1645    // ---- name resolution (local → class member → inherited → global) ----
1646
1647    fn resolve_name(&mut self, id: ExprId, name: &str) -> Ty {
1648        // Flow narrowing wins over the binding's declared type.
1649        if let Some(key) = self.narrow_key(id)
1650            && let Some(t) = self.narrowing.get(&key)
1651        {
1652            return t.clone();
1653        }
1654        if let Some(t) = self.locals.get(name) {
1655            return t.clone();
1656        }
1657        if let Some(item) = self.class.lookup(name) {
1658            return self.own_member_ty(item, false);
1659        }
1660        // Inherited members: an engine `Object` base via the API table, or a user `ScriptRef`
1661        // base via the script member walk (M2 — so a class extending another class_name sees its
1662        // inherited members).
1663        match self.class.base.clone() {
1664            Ty::Object(base) => {
1665                if let Some(m) = self.api.lookup_member(base, name) {
1666                    return self.member_ref_ty(&m, false);
1667                }
1668            }
1669            Ty::ScriptRef(base) => {
1670                if let Some(t) = self.script_member_walk(base, name, false, 0) {
1671                    return t;
1672                }
1673            }
1674            _ => {}
1675        }
1676        if let Some(g) = resolve::resolve_global(self.api, name) {
1677            return global_ty(&g);
1678        }
1679        // A project-global `class_name` used as a value — the class itself, for static access
1680        // (`V.fc()`) or as a constructor (`Player.new()`). Resolves to a `ScriptRef` via the
1681        // registry. Precedence (Godot `reduce_identifier`): `class_name` global ≫ autoload
1682        // singleton. So try `class_name` first, then a `*`-autoload, then the seam.
1683        let by_class = resolve::resolve_external(
1684            self.db,
1685            &resolve::ExternalRef::ClassName(SmolStr::new(name)),
1686        );
1687        if !by_class.is_unknown() {
1688            return by_class;
1689        }
1690        resolve::resolve_external(self.db, &resolve::ExternalRef::Autoload(SmolStr::new(name)))
1691    }
1692
1693    fn own_member_ty(&self, item: ClassItem, as_method: bool) -> Ty {
1694        match item {
1695            ClassItem::EnumVariant => self.int_ty(),
1696            ClassItem::Member(_) => match self.class.member(item) {
1697                Some(Member::Var(v)) => self.field_ty(&v.name, v.ptr),
1698                Some(Member::Const(c)) => self.field_ty(&c.name, c.ptr),
1699                Some(Member::Func(f)) => {
1700                    if as_method {
1701                        self.func_return_ty(f.return_type.as_deref())
1702                    } else {
1703                        Ty::Callable
1704                    }
1705                }
1706                Some(Member::Signal(_)) => Ty::Signal(None),
1707                Some(Member::Class(_)) => Ty::Unknown,
1708                Some(Member::Enum(_)) | None => Ty::Variant,
1709            },
1710        }
1711    }
1712
1713    /// The type of an own field (`var`/`const`): the type seeded by the field pre-pass (which
1714    /// captures the inferred type of `var n := 0`), falling back to the written annotation.
1715    fn field_ty(&self, name: &str, ptr: AstPtr) -> Ty {
1716        if let Some(t) = self.class.member_types.get(name) {
1717            return t.clone();
1718        }
1719        self.resolve_decl_annotation(ptr)
1720    }
1721
1722    /// Resolve a declaration's annotation (recovering its `TypeRef` node), else `Variant`.
1723    fn resolve_decl_annotation(&self, ptr: AstPtr) -> Ty {
1724        let Some(node) = ptr.to_node(self.root) else {
1725            return Ty::Variant;
1726        };
1727        cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
1728            .map_or(Ty::Variant, |t| {
1729                resolve::resolve_type_ref(self.db, self.api, &t)
1730            })
1731    }
1732
1733    // ---- narrowing ----
1734
1735    /// Apply the narrowing implied by an `if`/`elif` condition to the current (cloned) branch.
1736    ///
1737    /// `is`-narrowing is a deliberate divergence from upstream Godot (whose `is` does **not**
1738    /// flow-narrow); we add it as a UX improvement but keep it **widen-only** so it never produces
1739    /// a type Godot's checker would reject: narrow only when the tested type is a strict downcast
1740    /// of the operand's current type, or the operand is uninformative. If the operand is already a
1741    /// subtype of the test (`d: Derived; if d is Base`), keep it — do not un-narrow. The
1742    /// `is_uninformative` guard also stays: never narrow to a type we couldn't resolve.
1743    fn apply_narrowing(&mut self, cond: ExprId) {
1744        let Expr::Is {
1745            operand,
1746            ty: Some(ptr),
1747            negated: false,
1748        } = self.body.expr(cond).clone()
1749        else {
1750            return;
1751        };
1752        let Some(key) = self.narrow_key(operand) else {
1753            return;
1754        };
1755        let narrowed = self.resolve_ptr_ty(ptr);
1756        if narrowed.is_uninformative() {
1757            return;
1758        }
1759        let cur = self.expr_ty.get(&operand).cloned().unwrap_or(Ty::Variant);
1760        if cur.is_uninformative() || self.is_subtype(&narrowed, &cur) {
1761            self.narrowing.insert(key, narrowed);
1762        }
1763    }
1764
1765    /// A dotted access-path key for narrowing (`x`, `self.field`, `a.b.c`), or `None` for a
1766    /// non-path expression.
1767    fn narrow_key(&self, id: ExprId) -> Option<String> {
1768        match self.body.expr(id) {
1769            Expr::Name(n) => Some(n.to_string()),
1770            Expr::SelfExpr => Some("self".to_owned()),
1771            Expr::Paren(inner) => self.narrow_key(*inner),
1772            Expr::Field { receiver, name, .. } => {
1773                Some(format!("{}.{name}", self.narrow_key(*receiver)?))
1774            }
1775            _ => None,
1776        }
1777    }
1778
1779    fn resolve_ptr_ty(&self, ptr: AstPtr) -> Ty {
1780        ptr.to_node(self.root).map_or(Ty::Variant, |n| {
1781            resolve::resolve_type_ref(self.db, self.api, &n)
1782        })
1783    }
1784
1785    // ---- helpers ----
1786
1787    /// The join (least upper bound) of two branch types — conservative: equal types collapse,
1788    /// a subtype widens to its supertype, else `Variant`.
1789    ///
1790    /// The three uninformative markers do NOT collapse to `Variant` — that would defeat the
1791    /// seam. They propagate by priority: `Error` (already diagnosed) → `Unknown` (the cross-file
1792    /// seam — must never warn or cascade) → `Variant` (the gradual top). So
1793    /// `x if c else <unknown>` stays `Unknown`, and `var y := (x if c else unknown)` does not
1794    /// fire a false `INFERENCE_ON_VARIANT`.
1795    fn join(&self, a: &Ty, b: &Ty) -> Ty {
1796        if a == b {
1797            return a.clone();
1798        }
1799        if a.is_error() || b.is_error() {
1800            return Ty::Error;
1801        }
1802        if a.is_unknown() || b.is_unknown() {
1803            return Ty::Unknown;
1804        }
1805        if a.is_variant() || b.is_variant() {
1806            return Ty::Variant;
1807        }
1808        if ty::is_assignable(self.api, a, b) == Assign::Ok {
1809            return b.clone();
1810        }
1811        if ty::is_assignable(self.api, b, a) == Assign::Ok {
1812            return a.clone();
1813        }
1814        Ty::Variant
1815    }
1816
1817    /// Run a closure within a branch-scoped narrowing frame (clone on enter, restore on exit).
1818    fn in_branch<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
1819        let saved = self.narrowing.clone();
1820        let r = f(self);
1821        self.narrowing = saved;
1822        r
1823    }
1824}
1825
1826/// Map a resolved global definition to the type of a bare reference to it.
1827fn global_ty(g: &GlobalDef) -> Ty {
1828    match g {
1829        GlobalDef::Const(t) => t.clone(),
1830        GlobalDef::Singleton(c) | GlobalDef::ClassType(c) => Ty::Object(*c),
1831        GlobalDef::BuiltinType(b) => Ty::Builtin(*b),
1832        // A bare function referenced as a value is a `Callable`; an enum namespace is opaque.
1833        GlobalDef::Builtin | GlobalDef::Utility => Ty::Callable,
1834        GlobalDef::GlobalEnum => Ty::Variant,
1835    }
1836}
1837
1838fn inference_on_variant_msg(kind: &str) -> String {
1839    format!(
1840        "The {kind} type is being inferred from a Variant value, so it will be typed as Variant."
1841    )
1842}
1843
1844/// The `extension_api.json` operator spelling for a binary operator.
1845fn op_symbol(op: BinOp) -> Option<&'static str> {
1846    Some(match op {
1847        BinOp::Add => "+",
1848        BinOp::Sub => "-",
1849        BinOp::Mul => "*",
1850        BinOp::Div => "/",
1851        BinOp::Mod => "%",
1852        BinOp::Pow => "**",
1853        BinOp::BitAnd => "&",
1854        BinOp::BitOr => "|",
1855        BinOp::BitXor => "^",
1856        BinOp::Shl => "<<",
1857        BinOp::Shr => ">>",
1858        _ => return None,
1859    })
1860}
1861
1862#[cfg(test)]
1863mod tests {
1864    use super::*;
1865    use crate::item_tree::item_tree;
1866    use gdscript_syntax::{SyntaxKind, parse};
1867
1868    struct Harness {
1869        result: InferenceResult,
1870        body: Body,
1871    }
1872
1873    /// Infer the (first) function in `src` against a fresh class scope.
1874    fn infer_first_func(src: &str) -> Harness {
1875        let api = gdscript_api::bundled();
1876        let db = gdscript_db::RootDatabase::default();
1877        let root = parse(src).syntax_node();
1878        let tree = item_tree(&root);
1879        let class = ClassScope::new(&db, api, &tree, None);
1880        let func = gdscript_syntax::ast::descendants(&root)
1881            .into_iter()
1882            .find(|n| n.kind() == SyntaxKind::FuncDecl)
1883            .expect("a function");
1884        let body = body::body_of_func(&func);
1885        let return_ty = cst::first_child(&func, |k| k == SyntaxKind::TypeRef)
1886            .map_or(Ty::Variant, |t| resolve::resolve_type_ref(&db, api, &t));
1887        let result = infer(&db, api, &root, &class, &body, return_ty);
1888        Harness { result, body }
1889    }
1890
1891    fn codes(h: &Harness) -> Vec<&str> {
1892        h.result
1893            .diagnostics
1894            .iter()
1895            .map(|d| d.code.as_str())
1896            .collect()
1897    }
1898
1899    /// Run the whole-file pass (Pass 1 field fixpoint + Pass 2 functions) and collect every
1900    /// diagnostic code. Drives `analyze_file` directly so the bounded member fixpoint runs.
1901    fn file_codes(src: &str) -> Vec<String> {
1902        let api = gdscript_api::bundled();
1903        let db = gdscript_db::RootDatabase::default();
1904        let root = parse(src).syntax_node();
1905        let fi = analyze_file(&db, api, &root, FileId(0));
1906        fi.diagnostics.iter().map(|d| d.code.clone()).collect()
1907    }
1908
1909    #[test]
1910    fn integer_division_warns() {
1911        let h = infer_first_func("func f():\n\tvar x = 5 / 2\n");
1912        assert!(codes(&h).contains(&INTEGER_DIVISION));
1913    }
1914
1915    #[test]
1916    fn float_div_does_not_warn() {
1917        let h = infer_first_func("func f():\n\tvar x = 5.0 / 2\n");
1918        assert!(!codes(&h).contains(&INTEGER_DIVISION));
1919    }
1920
1921    #[test]
1922    fn type_mismatch_on_hard_annotation() {
1923        let h = infer_first_func("func f():\n\tvar s: String = 5\n");
1924        assert!(codes(&h).contains(&TYPE_MISMATCH));
1925    }
1926
1927    #[test]
1928    fn narrowing_conversion_float_to_int() {
1929        let h = infer_first_func("func f():\n\tvar n: int = 1.5\n");
1930        assert!(codes(&h).contains(&NARROWING_CONVERSION));
1931    }
1932
1933    #[test]
1934    fn int_to_float_is_silent() {
1935        let h = infer_first_func("func f():\n\tvar x: float = 3\n");
1936        assert!(
1937            h.result.diagnostics.is_empty(),
1938            "{:?}",
1939            h.result.diagnostics
1940        );
1941    }
1942
1943    #[test]
1944    fn member_access_resolves_engine_property() {
1945        // In a Node script, bare `get_node(...)` resolves via the inherited base to Object(Node);
1946        // `get_parent()` is a real Node method → no UNSAFE.
1947        let h = infer_first_func(
1948            "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.get_parent()\n",
1949        );
1950        assert!(
1951            codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
1952            "{:?}",
1953            h.result.diagnostics
1954        );
1955    }
1956
1957    #[test]
1958    fn unsafe_method_on_known_type() {
1959        let h = infer_first_func(
1960            "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.totally_bogus_method()\n",
1961        );
1962        assert!(
1963            codes(&h).contains(&UNSAFE_METHOD_ACCESS),
1964            "{:?}",
1965            h.result.diagnostics
1966        );
1967    }
1968
1969    #[test]
1970    fn is_narrowing_suppresses_unsafe() {
1971        // Without narrowing, `x.free()` on an untyped param would be unchecked anyway; with
1972        // `is Node` it is checked against Node and `free` IS a Node method → no UNSAFE.
1973        let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.queue_free()\n");
1974        assert!(
1975            codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
1976            "{:?}",
1977            h.result.diagnostics
1978        );
1979    }
1980
1981    #[test]
1982    fn is_narrowing_flags_real_missing_member() {
1983        // After `is Node`, x is Node; `.bogus()` is genuinely missing → UNSAFE.
1984        let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.bogus_method()\n");
1985        assert!(codes(&h).contains(&UNSAFE_METHOD_ACCESS));
1986    }
1987
1988    #[test]
1989    fn variant_receiver_never_unsafe() {
1990        // Untyped param → Variant receiver → unchecked, no diagnostic.
1991        let h = infer_first_func("func f(x):\n\tx.anything_at_all()\n");
1992        assert!(
1993            h.result.diagnostics.is_empty(),
1994            "{:?}",
1995            h.result.diagnostics
1996        );
1997    }
1998
1999    #[test]
2000    fn unsafe_call_argument_on_variant_into_typed_param() {
2001        // Passing an untyped (Variant) value to a typed own-method parameter needs an unsafe cast.
2002        let h = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n: Node2D):\n\tpass\n");
2003        assert!(
2004            codes(&h).contains(&UNSAFE_CALL_ARGUMENT),
2005            "{:?}",
2006            h.result.diagnostics
2007        );
2008    }
2009
2010    #[test]
2011    fn unsafe_call_argument_silent_on_safe_and_untyped() {
2012        // A subtype arg (upcast) is safe; an untyped parameter accepts anything — neither warns.
2013        let upcast =
2014            infer_first_func("func f(n: Node2D):\n\ttake(n)\nfunc take(n: Node):\n\tpass\n");
2015        assert!(
2016            !codes(&upcast).contains(&UNSAFE_CALL_ARGUMENT),
2017            "upcast is safe: {:?}",
2018            upcast.result.diagnostics
2019        );
2020        let untyped = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n):\n\tpass\n");
2021        assert!(
2022            !codes(&untyped).contains(&UNSAFE_CALL_ARGUMENT),
2023            "untyped param accepts anything: {:?}",
2024            untyped.result.diagnostics
2025        );
2026    }
2027
2028    #[test]
2029    fn inference_on_variant() {
2030        // `:=` from an untyped (Variant) param.
2031        let h = infer_first_func("func f(x):\n\tvar y := x\n");
2032        assert!(codes(&h).contains(&INFERENCE_ON_VARIANT));
2033    }
2034
2035    #[test]
2036    fn field_inferred_from_earlier_field_is_typed() {
2037        // W2-MEMBER-FIXPOINT: `b`'s initializer references the earlier field `a`. A single shallow
2038        // field pass would see `a` as `Variant` (seam) and fire INFERENCE_ON_VARIANT on `:= a`; the
2039        // bounded fixpoint seeds `a: int` so `a + 1` is `int` and `:=` is precise — no warning.
2040        let codes = file_codes("var a := 1\nvar b := a + 1\n");
2041        assert!(
2042            !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
2043            "field `b` from earlier field `a` should type as int, not Variant: {codes:?}"
2044        );
2045    }
2046
2047    #[test]
2048    fn field_forward_reference_is_seamed_not_warned() {
2049        // A field referencing a *later* field still resolves through the fixpoint (both rounds
2050        // see each other's seeded type), and at worst lands on the conservative seam — never a
2051        // false INFERENCE_ON_VARIANT. (`b` precedes `a` lexically here.)
2052        let codes = file_codes("var b := a\nvar a := 1\n");
2053        assert!(
2054            !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
2055            "forward field reference must not false-warn: {codes:?}"
2056        );
2057    }
2058
2059    #[test]
2060    fn standalone_inferred_field_unchanged() {
2061        // No-regression: a self-contained inferred field still types from its literal, no warning.
2062        let codes = file_codes("var n := 0\n");
2063        assert!(
2064            codes.is_empty(),
2065            "a literal-initialised field should produce no diagnostics: {codes:?}"
2066        );
2067    }
2068
2069    #[test]
2070    fn lambda_var_is_callable_not_variant() {
2071        let h = infer_first_func("func f():\n\tvar cb := func():\n\t\tpass\n");
2072        assert!(
2073            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2074            "{:?}",
2075            h.result.diagnostics
2076        );
2077    }
2078
2079    #[test]
2080    fn multiline_lambda_then_paren_line_no_false_warning() {
2081        // A multi-line lambda bound to a var, followed by a statement that begins with `(`.
2082        // The parser now ends the lambda at its dedent (the `(` line is its own statement), so
2083        // there is no spurious call-on-lambda and no false `INFERENCE_ON_VARIANT`.
2084        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";
2085        let h = infer_first_func(src);
2086        assert!(
2087            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2088            "{:?}",
2089            h.result.diagnostics
2090        );
2091    }
2092
2093    #[test]
2094    fn calling_a_callable_value_is_seam_not_variant() {
2095        // Invoking an arbitrary expression (here a parenthesized `Callable` value) reaches the
2096        // seam arm of `infer_call`: the return type isn't tracked, so the result is Unknown,
2097        // not `Variant`, and the inferred-on-Variant warning never fires.
2098        let src = "func f(cb: Callable):\n\tvar x := (cb)()\n\treturn x\n";
2099        let h = infer_first_func(src);
2100        assert!(
2101            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2102            "{:?}",
2103            h.result.diagnostics
2104        );
2105    }
2106
2107    #[test]
2108    fn ternary_with_seam_branch_does_not_collapse_to_variant() {
2109        // A ternary whose else-branch is the seam (`await` is untracked → Unknown) must `join`
2110        // to Unknown, NOT Variant — otherwise `var x := …` fires a false INFERENCE_ON_VARIANT.
2111        // (Regression: `join` used to absorb any uninformative branch to Variant.)
2112        let src =
2113            "func f(c: bool):\n\tvar x := 5 if c else await get_tree().process_frame\n\treturn x\n";
2114        let h = infer_first_func(src);
2115        assert!(
2116            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2117            "seam branch should keep the ternary on the seam: {:?}",
2118            h.result.diagnostics
2119        );
2120    }
2121
2122    #[test]
2123    fn await_a_coroutine_call_recovers_its_return_type() {
2124        // `await f()` yields the call's value, so await is identity on a non-signal operand:
2125        // `await make()` for `func make() -> int` types `x` as int (was the seam before).
2126        let src = "func g() -> int:\n\tvar x := await make()\n\treturn x\nfunc make() -> int:\n\treturn 5\n";
2127        let h = infer_first_func(src);
2128        assert!(
2129            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2130            "no false variant warning: {:?}",
2131            h.result.diagnostics
2132        );
2133        let api = gdscript_api::bundled();
2134        let x = &h.result.bindings[0];
2135        assert!(
2136            matches!(&x.ty, Ty::Builtin(b) if api.builtin(*b).name == "int"),
2137            "await make() should recover int, got {:?}",
2138            x.ty
2139        );
2140    }
2141
2142    #[test]
2143    fn await_a_signal_stays_the_seam() {
2144        // `await sig` yields the signal's payload (needs the Phase-3+ sig table) — must stay the seam,
2145        // never the Signal type itself, and never a false INFERENCE_ON_VARIANT.
2146        let src = "func f():\n\tvar x := await get_tree().process_frame\n\treturn x\n";
2147        let h = infer_first_func(src);
2148        assert!(
2149            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2150            "awaiting a signal must not warn: {:?}",
2151            h.result.diagnostics
2152        );
2153        assert!(
2154            matches!(&h.result.bindings[0].ty, Ty::Unknown),
2155            "awaiting a signal stays the seam, got {:?}",
2156            h.result.bindings[0].ty
2157        );
2158    }
2159
2160    #[test]
2161    fn for_var_over_packed_string_array_is_string() {
2162        // `for s in "a,b".split(",")` iterates a PackedStringArray → String, so `s.to_int()`
2163        // resolves and `var x := s` does not warn.
2164        let h = infer_first_func("func f():\n\tfor s in \"a,b\".split(\",\"):\n\t\tvar x := s\n");
2165        assert!(
2166            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2167            "{:?}",
2168            h.result.diagnostics
2169        );
2170    }
2171
2172    #[test]
2173    fn class_new_is_object_not_variant() {
2174        let h = infer_first_func("func f():\n\tvar s := GDScript.new()\n");
2175        assert!(
2176            !codes(&h).contains(&INFERENCE_ON_VARIANT),
2177            "{:?}",
2178            h.result.diagnostics
2179        );
2180    }
2181
2182    #[test]
2183    fn unknown_seam_never_warns() {
2184        // `preload(...)` is Unknown; `:=` from it does NOT warn, and member access is unchecked.
2185        let h = infer_first_func("func f():\n\tvar s := preload(\"res://x.gd\")\n\ts.whatever()\n");
2186        assert!(
2187            h.result.diagnostics.is_empty(),
2188            "{:?}",
2189            h.result.diagnostics
2190        );
2191    }
2192
2193    #[test]
2194    fn expr_types_are_memoized_for_hover() {
2195        let h = infer_first_func("func f():\n\tvar n := 42\n");
2196        // The `42` literal expr should be typed int.
2197        let has_int = h
2198            .result
2199            .expr_ty
2200            .values()
2201            .any(|t| matches!(t, Ty::Builtin(_)));
2202        assert!(has_int);
2203        // sanity: the body lowered at least one expr
2204        assert!(!h.body.exprs.is_empty());
2205    }
2206}