const std = @import("std");
const ast = @import("../ast.zig");
const errors = @import("../errors.zig");
const types = @import("types.zig");
const name_class = @import("../types.zig");
const program_index_mod = @import("program_index.zig");
const type_resolver = @import("type_resolver.zig");

const Node = ast.Node;
const TypeTable = types.TypeTable;
const ProgramIndex = program_index_mod.ProgramIndex;
const TypeResolver = type_resolver.TypeResolver;

/// Declaration-name / type-position diagnostic pass. Two checks, before
/// lowering:
///
/// 1. Unknown-type diagnostic, extracted from `Lowering`
///    (architecture phase A2.4): an identifier used in a type position that
///    names no declared type, primitive, or in-scope generic type parameter.
///    Main-file decls only — imported / library modules are trusted, matching
///    `checkErrorFlow`.
/// 2. Reserved-type-name binding: a value binding
///    (local/global `var`, a typed-local, or a parameter) spelled as a
///    reserved/builtin type name. See `isReservedTypeName`. Runs over EVERY
///    compiled module (no main-file filter): such a binding mis-lowers the same
///    way wherever declared, so an imported module or the stdlib is no
///    exception.
///
/// Without (1)'s check, `TypeResolver.resolveNamed`'s empty-struct-stub fallback silently
/// fabricates a 0-field struct named after the unknown identifier — so a value
/// param mistakenly used as a type (`(T: Type, …) -> T`, missing the `$`) or a
/// typo'd type name compiles and runs, rendering as `T{}`. Main-file decls only;
/// imported / library modules are trusted, matching `checkErrorFlow`.
///
/// Queries the canonical facts rather than maintaining a parallel authoritative
/// list: declared top-level names come from `ProgramIndex` (runtime classes,
/// generic templates, protocols, aliases) plus the AST decl/scope walk (for
/// LOCAL type decls, which `ProgramIndex` doesn't track); primitives come from
/// `TypeResolver.resolvePrimitive`; registered concrete types from the
/// `TypeTable`. Constructed by value with borrowed references; built only when
/// diagnostics are active.
pub const UnknownTypeChecker = struct {
    alloc: std.mem.Allocator,
    diagnostics: *errors.DiagnosticList,
    types: *TypeTable,
    index: *ProgramIndex,
    main_file: ?[]const u8,

    pub fn run(self: UnknownTypeChecker, decls: []const *const Node) void {
        // Reserved-type-name binding diagnostic: rejects any
        // parameter name or `var` / `:=` / typed-local binding name spelled as a
        // reserved/builtin type name. Runs over EVERY compiled module — imported
        // user modules and the stdlib `library/` included — because such a
        // binding mis-lowers identically wherever it is declared: a loaded
        // aggregate passed by value to a `ptr` param → LLVM verifier abort. No
        // main-file filter (unlike the unknown-type check below) and no declared-
        // type / scope context — rejection is purely on spelling. The walk
        // tracks each module's source file (via the diagnostic list's
        // `current_source_file`, saved/restored per node) so an imported-module
        // diagnostic renders against that module's text.
        for (decls) |decl| self.checkBindingNames(decl);

        // Unknown-type diagnostic: main-file decls only; imported
        // and library modules are trusted, matching `checkErrorFlow`.
        var declared = std.StringHashMap(void).init(self.alloc);
        defer declared.deinit();
        self.collectDeclaredTypeNames(decls, &declared);
        const saved_file = self.diagnostics.current_source_file;
        defer self.diagnostics.current_source_file = saved_file;
        for (decls) |decl| {
            if (self.main_file) |mf| {
                if (decl.source_file) |sf| {
                    if (!std.mem.eql(u8, sf, mf)) continue;
                }
            }
            // Render against the decl's own module, not the ambient file the
            // previous phase left behind (issue 0122).
            if (decl.source_file) |sf| self.diagnostics.current_source_file = sf;
            switch (decl.data) {
                .fn_decl => self.checkFnSignatureTypes(&decl.data.fn_decl, &declared),
                .struct_decl => |sd| self.checkStructFieldTypes(&sd, &declared),
                .const_decl => |cd| switch (cd.value.data) {
                    .fn_decl => self.checkFnSignatureTypes(&cd.value.data.fn_decl, &declared),
                    .struct_decl => |sd| self.checkStructFieldTypes(&sd, &declared),
                    else => {},
                },
                else => {},
            }
        }
    }

    /// Reserved-type-name binding walk. Visits every node
    /// reachable from `node` and rejects each *binding name* — `var` / `:=` /
    /// typed-local declarations, destructure names, function / lambda / method
    /// parameters, `if` / `while` optional bindings, `for` capture + index
    /// names, match-arm captures, and `catch` / `onfail` tag bindings — whose
    /// spelling collides with a reserved/builtin type name. Such a spelling
    /// parses as a `.type_expr`, so the address-of family in `lower.zig` never
    /// sees the scoped local and mis-lowers it (a loaded aggregate passed
    /// by value to a `ptr` param → LLVM verifier abort, or a silent
    /// mutation-losing copy). Rejecting the name here, before lowering, keeps
    /// the `.identifier`-only address-of paths correct with no lowering
    /// special-case.
    ///
    /// The `switch` is EXHAUSTIVE — every `Node.Data` tag is listed and there
    /// is NO `else` arm. A future binding-bearing node type therefore fails to
    /// compile here until it is handled, so coverage is enforced by the
    /// compiler rather than by remembering to extend a hand-maintained list.
    /// (The check can't live at the scope-registration choke point in
    /// `lower.zig`: lowering is lazy, so an UNCALLED function's bindings never
    /// reach `Scope.put` — yet they must still be rejected at their
    /// declaration.) Deliberately filter-free (every compiled module is walked)
    /// and context-free (spelling is the sole criterion), distinct from the
    /// main-file-scoped unknown-type walk. A node carrying its own
    /// `source_file` (every module's top-level decls do) becomes the emit file
    /// for its whole subtree, restored on exit so a sibling in another module
    /// isn't rendered against it.
    fn checkBindingNames(self: UnknownTypeChecker, node: *const Node) void {
        const saved_file = self.diagnostics.current_source_file;
        defer self.diagnostics.current_source_file = saved_file;
        if (node.source_file) |sf| self.diagnostics.current_source_file = sf;
        switch (node.data) {
            // ── Binding-introducing nodes: check the name(s), then recurse. ──
            // Every site passes the node's own `is_raw` straight to the check —
            // never an `if (!is_raw)` call-site guard — so the check and its
            // exemption are one operation that cannot be threaded apart (0089).
            .var_decl => |vd| {
                self.checkBindingName(vd.name, vd.name_span, vd.is_raw);
                if (vd.value) |v| self.checkBindingNames(v);
            },
            .destructure_decl => |dd| {
                for (dd.names, dd.name_spans, dd.name_is_raw) |n, sp, raw| {
                    self.checkBindingName(n, sp, raw);
                }
                self.checkBindingNames(dd.value);
            },
            .fn_decl => |fd| {
                // A function NAME is a binding site too: a bare reserved-name
                // `i2 :: (…) {…}` (free fn or struct/impl method) is rejected,
                // exactly like `i2 := …`. Backtick (`` `i2 :: … ``) and
                // `#import c` extern fns set `is_raw` and are exempt (0089).
                self.checkBindingName(fd.name, fd.name_span, fd.is_raw);
                self.checkParamNames(fd.params);
                self.checkBindingNames(fd.body);
            },
            .lambda => |lm| {
                self.checkParamNames(lm.params);
                self.checkBindingNames(lm.body);
            },
            .param => |p| {
                self.checkBindingName(p.name, p.name_span, p.is_raw);
                if (p.default_expr) |de| self.checkBindingNames(de);
            },
            .if_expr => |ie| {
                if (ie.binding_name) |bn| self.checkBindingName(bn, ie.binding_span, ie.binding_is_raw);
                self.checkBindingNames(ie.condition);
                self.checkBindingNames(ie.then_branch);
                if (ie.else_branch) |e| self.checkBindingNames(e);
            },
            .while_expr => |we| {
                if (we.binding_name) |bn| self.checkBindingName(bn, we.binding_span, we.binding_is_raw);
                self.checkBindingNames(we.condition);
                self.checkBindingNames(we.body);
            },
            .for_expr => |fe| {
                for (fe.captures) |cap| {
                    if (cap.name.len != 0) self.checkBindingName(cap.name, cap.span, cap.is_raw);
                }
                for (fe.iterables) |it| {
                    self.checkBindingNames(it.expr);
                    if (it.range_end) |re| self.checkBindingNames(re);
                }
                self.checkBindingNames(fe.body);
            },
            .match_expr => |me| {
                self.checkBindingNames(me.subject);
                for (me.arms) |arm| {
                    if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span, arm.capture_is_raw);
                    if (arm.pattern) |p| self.checkBindingNames(p);
                    self.checkBindingNames(arm.body);
                }
            },
            .match_arm => |arm| {
                if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span, arm.capture_is_raw);
                if (arm.pattern) |p| self.checkBindingNames(p);
                self.checkBindingNames(arm.body);
            },
            .catch_expr => |ce| {
                if (ce.binding) |b| self.checkBindingName(b, ce.binding_span, ce.binding_is_raw);
                self.checkBindingNames(ce.operand);
                self.checkBindingNames(ce.body);
            },
            .onfail_stmt => |os| {
                if (os.binding) |b| self.checkBindingName(b, os.binding_span, os.binding_is_raw);
                self.checkBindingNames(os.body);
            },
            // impl / protocol-default / runtime-class method bodies: each
            // method introduces its own params + locals. A `#jni_main` /
            // `#objc_class` bodied method is lowered (M1.2), so its reserved
            // param/local names mis-lower the same as any other.
            .impl_block => |ib| for (ib.methods) |m| self.checkBindingNames(m),
            .protocol_decl => |pd| {
                self.checkDeclName(node, pd.name, pd.is_raw);
                for (pd.methods) |m| {
                    if (m.default_body) |body| {
                        for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| {
                            const raw = i < m.param_name_is_raw.len and m.param_name_is_raw[i];
                            self.checkBindingName(pn, sp, raw);
                        }
                        self.checkBindingNames(body);
                    }
                }
            },
            .runtime_class_decl => |fcd| {
                // The sx-side alias (left of `::`) is a user-chosen name, so a
                // reserved spelling is rejected like any other type decl (0089).
                self.checkDeclName(node, fcd.name, fcd.is_raw);
                for (fcd.members) |member| switch (member) {
                    .method => |m| if (m.body) |body| {
                        for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| {
                            const raw = i < m.param_name_is_raw.len and m.param_name_is_raw[i];
                            self.checkBindingName(pn, sp, raw);
                        }
                        self.checkBindingNames(body);
                    },
                    .field, .extends, .implements => {},
                };
            },
            // ── Container / control-flow / expression nodes: recurse children
            //    so a binding nested anywhere below is still reached. ──
            // A namespaced import (`mod :: #import "..."`) is wrapped here, its
            // module decls held inline; descend so an imported module's
            // reserved-name binding is rejected too.
            .namespace_decl => |nd| {
                self.checkDeclName(node, nd.name, nd.is_raw);
                for (nd.decls) |d| self.checkBindingNames(d);
            },
            .const_decl => |cd| {
                // A const BINDS `cd.name`. Reject a bare reserved spelling
                // unless it is backtick-raw (`cd.is_raw`) or the compiler's
                // blessed builtin definition (`string :: []u8 #builtin`, value
                // `.builtin_expr`). When the value node is itself a named decl
                // (struct/enum/union/error/fn), that node carries & checks its
                // own name on recursion — don't double-check it here (0089).
                switch (cd.value.data) {
                    .builtin_expr, .struct_decl, .enum_decl, .union_decl, .error_set_decl, .fn_decl => {},
                    else => self.checkBindingName(cd.name, cd.name_span, cd.is_raw),
                }
                self.checkBindingNames(cd.value);
            },
            .struct_decl => |sd| {
                self.checkDeclName(node, sd.name, sd.is_raw);
                for (sd.methods) |m| self.checkBindingNames(m);
                for (sd.constants) |c| self.checkBindingNames(c);
                for (sd.field_defaults) |fdef| if (fdef) |d| self.checkBindingNames(d);
            },
            .root => |r| for (r.decls) |d| self.checkBindingNames(d),
            .block => |b| for (b.stmts) |s| self.checkBindingNames(s),
            .push_stmt => |ps| {
                self.checkBindingNames(ps.context_expr);
                self.checkBindingNames(ps.body);
            },
            .jni_env_block => |jb| {
                self.checkBindingNames(jb.env);
                self.checkBindingNames(jb.body);
            },
            .defer_stmt => |ds| self.checkBindingNames(ds.expr),
            .return_stmt => |r| if (r.value) |v| self.checkBindingNames(v),
            .raise_stmt => |rs| self.checkBindingNames(rs.tag),
            .assignment => |a| {
                self.checkBindingNames(a.value);
                self.checkBindingNames(a.target);
            },
            .multi_assign => |ma| {
                for (ma.targets) |t| self.checkBindingNames(t);
                for (ma.values) |v| self.checkBindingNames(v);
            },
            .call => |c| {
                self.checkBindingNames(c.callee);
                for (c.args) |a| self.checkBindingNames(a);
            },
            .ffi_intrinsic_call => |fic| for (fic.args) |a| self.checkBindingNames(a),
            .binary_op => |b| {
                self.checkBindingNames(b.lhs);
                self.checkBindingNames(b.rhs);
            },
            .chained_comparison => |cc| for (cc.operands) |o| self.checkBindingNames(o),
            .unary_op => |u| self.checkBindingNames(u.operand),
            .field_access => |fa| self.checkBindingNames(fa.object),
            .index_expr => |ix| {
                self.checkBindingNames(ix.object);
                self.checkBindingNames(ix.index);
            },
            .slice_expr => |sx| {
                self.checkBindingNames(sx.object);
                if (sx.start) |s| self.checkBindingNames(s);
                if (sx.end) |e| self.checkBindingNames(e);
            },
            .struct_literal => |sl| {
                for (sl.field_inits) |fi| self.checkBindingNames(fi.value);
                if (sl.init_block) |ib| self.checkBindingNames(ib);
            },
            .array_literal => |al| for (al.elements) |e| self.checkBindingNames(e),
            .tuple_literal => |tl| for (tl.elements) |e| self.checkBindingNames(e.value),
            .force_unwrap => |fu| self.checkBindingNames(fu.operand),
            .null_coalesce => |nc| {
                self.checkBindingNames(nc.lhs);
                self.checkBindingNames(nc.rhs);
            },
            .deref_expr => |de| self.checkBindingNames(de.operand),
            .try_expr => |te| self.checkBindingNames(te.operand),
            .comptime_expr => |ce| self.checkBindingNames(ce.expr),
            .insert_expr => |ins| self.checkBindingNames(ins.expr),
            .spread_expr => |se| self.checkBindingNames(se.operand),
            .asm_expr => |ae| {
                self.checkBindingNames(ae.template);
                for (ae.operands) |op| self.checkBindingNames(op.payload);
            },
            .asm_global => |ag| self.checkBindingNames(ag.template),
            // ── Named type / alias / import declarations: a bare reserved
            // spelling as the declared name is rejected. These
            //    have no nested binding sites, so only the name is checked. A
            //    flat `#import`/`#import c` (name == null) binds nothing. ──
            .enum_decl => |ed| self.checkDeclName(node, ed.name, ed.is_raw),
            .union_decl => |ud| self.checkDeclName(node, ud.name, ud.is_raw),
            .error_set_decl => |esd| self.checkDeclName(node, esd.name, esd.is_raw),
            .ufcs_alias => |ua| self.checkDeclName(node, ua.name, ua.is_raw),
            .library_decl => |ld| self.checkDeclName(node, ld.name, ld.is_raw),
            .import_decl => |imp| if (imp.name) |n| self.checkDeclName(node, n, imp.is_raw),
            .c_import_decl => |cid| if (cid.name) |n| self.checkDeclName(node, n, cid.is_raw),
            // ── Leaves & pure type-expression nodes: no binding sites below. ──
            // Type-expression subtrees carry only type names (no value
            // bindings). Listing each tag explicitly (rather than an `else`) is
            // what forces a future binding-bearing node to be reconsidered here.
            .int_literal,
            .float_literal,
            .bool_literal,
            .string_literal,
            .identifier,
            .enum_literal,
            .type_expr,
            .array_type_expr,
            .slice_type_expr,
            .parameterized_type_expr,
            .pointer_type_expr,
            .many_pointer_type_expr,
            .optional_type_expr,
            .error_type_expr,
            .caller_location,
            .pack_index_type_expr,
            .comptime_pack_ref,
            .null_literal,
            .break_expr,
            .continue_expr,
            .undef_literal,
            .inferred_type,
            .builtin_expr,
            .framework_decl,
            .function_type_expr,
            .closure_type_expr,
            .tuple_type_expr,
            => {},
        }
    }

    /// Check each parameter's binding name (`fn` / lambda params are stored as
    /// `Param` values, not child nodes, so they're walked here rather than via
    /// the node `switch`). A param default expression can itself nest bindings
    /// (a lambda default), so recurse into it.
    fn checkParamNames(self: UnknownTypeChecker, params: []const ast.Param) void {
        for (params) |p| {
            // A backtick raw param (`` (`i2: T) ``) or a `#import c` extern
            // param is exempt from the reserved-type-name rule —
            // the exemption is honored inside `checkBindingName` via `p.is_raw`.
            self.checkBindingName(p.name, p.name_span, p.is_raw);
            if (p.default_expr) |de| self.checkBindingNames(de);
        }
    }

    /// Collect every top-level name that can legitimately appear in a type
    /// position: const-decl names (covers `T :: struct/enum/union/error/alias`
    /// and value consts), plus the scan-populated runtime-class / generic-
    /// template / protocol / alias maps from `ProgramIndex`. Built across ALL
    /// files so a main-file reference to an imported type isn't flagged.
    fn collectDeclaredTypeNames(self: UnknownTypeChecker, decls: []const *const Node, out: *std.StringHashMap(void)) void {
        for (decls) |decl| {
            switch (decl.data) {
                .const_decl => |cd| {
                    // Only a const whose VALUE introduces a type (a type decl or
                    // type-expression alias) declares a type name. A value const
                    // like `NotAType :: 123` must NOT satisfy the unknown-type
                    // check.
                    if (constValueIntroducesType(cd.value)) out.put(cd.name, {}) catch {};
                    if (cd.value.data == .fn_decl) self.harvestScopeDecls(cd.value.data.fn_decl.body, out);
                },
                .struct_decl => |sd| out.put(sd.name, {}) catch {},
                .fn_decl => |fd| self.harvestScopeDecls(fd.body, out),
                else => {},
            }
        }
        var it_fc = self.index.runtime_class_map.keyIterator();
        while (it_fc.next()) |k| out.put(k.*, {}) catch {};
        var it_tmpl = self.index.struct_template_map.keyIterator();
        while (it_tmpl.next()) |k| out.put(k.*, {}) catch {};
        var it_pd = self.index.protocol_decl_map.keyIterator();
        while (it_pd.next()) |k| out.put(k.*, {}) catch {};
        var it_pa = self.index.protocol_ast_map.keyIterator();
        while (it_pa.next()) |k| out.put(k.*, {}) catch {};
        var it_al = self.index.type_alias_map.keyIterator();
        while (it_al.next()) |k| out.put(k.*, {}) catch {};
    }

    /// Harvest every type-declaration name (local `T :: struct/enum/union` and
    /// named consts) anywhere in a function body — including inside nested
    /// closure / function bodies — into the global declared set, so a type
    /// annotation in any scope that references one isn't flagged. Over-collection
    /// is safe: it only ever relaxes the unknown-type check, never tightens it.
    fn harvestScopeDecls(self: UnknownTypeChecker, node: *const Node, out: *std.StringHashMap(void)) void {
        switch (node.data) {
            .block => |b| for (b.stmts) |s| self.harvestScopeDecls(s, out),
            .if_expr => |ie| {
                self.harvestScopeDecls(ie.condition, out);
                self.harvestScopeDecls(ie.then_branch, out);
                if (ie.else_branch) |e| self.harvestScopeDecls(e, out);
            },
            .while_expr => |we| {
                self.harvestScopeDecls(we.condition, out);
                self.harvestScopeDecls(we.body, out);
            },
            .for_expr => |fe| {
                for (fe.iterables) |it| {
                    self.harvestScopeDecls(it.expr, out);
                    if (it.range_end) |re| self.harvestScopeDecls(re, out);
                }
                self.harvestScopeDecls(fe.body, out);
            },
            .match_expr => |me| {
                self.harvestScopeDecls(me.subject, out);
                for (me.arms) |arm| self.harvestScopeDecls(arm.body, out);
            },
            .push_stmt => |ps| {
                self.harvestScopeDecls(ps.context_expr, out);
                self.harvestScopeDecls(ps.body, out);
            },
            .defer_stmt => |ds| self.harvestScopeDecls(ds.expr, out),
            .onfail_stmt => |os| self.harvestScopeDecls(os.body, out),
            .return_stmt => |r| if (r.value) |v| self.harvestScopeDecls(v, out),
            .raise_stmt => |rs| self.harvestScopeDecls(rs.tag, out),
            .assignment => |a| {
                self.harvestScopeDecls(a.value, out);
                self.harvestScopeDecls(a.target, out);
            },
            .multi_assign => |ma| for (ma.values) |v| self.harvestScopeDecls(v, out),
            .destructure_decl => |dd| self.harvestScopeDecls(dd.value, out),
            .var_decl => |vd| if (vd.value) |v| self.harvestScopeDecls(v, out),
            .const_decl => |cd| {
                // Local type decl (`T :: struct/enum/union/error/alias`) — add
                // its name; a local VALUE const (`x :: 5`) does not declare a
                // type. Recurse regardless, to harvest nested decls
                // (e.g. type decls inside a `f :: () { ... }` body).
                if (constValueIntroducesType(cd.value)) out.put(cd.name, {}) catch {};
                self.harvestScopeDecls(cd.value, out);
            },
            .struct_decl => |sd| out.put(sd.name, {}) catch {},
            .enum_decl => |ed| out.put(ed.name, {}) catch {},
            .union_decl => |ud| out.put(ud.name, {}) catch {},
            .call => |c| {
                self.harvestScopeDecls(c.callee, out);
                for (c.args) |a| self.harvestScopeDecls(a, out);
            },
            .binary_op => |b| {
                self.harvestScopeDecls(b.lhs, out);
                self.harvestScopeDecls(b.rhs, out);
            },
            .unary_op => |u| self.harvestScopeDecls(u.operand, out),
            .field_access => |fa| self.harvestScopeDecls(fa.object, out),
            .index_expr => |ix| {
                self.harvestScopeDecls(ix.object, out);
                self.harvestScopeDecls(ix.index, out);
            },
            .struct_literal => |sl| {
                for (sl.field_inits) |fi| self.harvestScopeDecls(fi.value, out);
                if (sl.init_block) |ib| self.harvestScopeDecls(ib, out);
            },
            .array_literal => |al| for (al.elements) |e| self.harvestScopeDecls(e, out),
            .force_unwrap => |fu| self.harvestScopeDecls(fu.operand, out),
            .null_coalesce => |nc| {
                self.harvestScopeDecls(nc.lhs, out);
                self.harvestScopeDecls(nc.rhs, out);
            },
            .deref_expr => |de| self.harvestScopeDecls(de.operand, out),
            .try_expr => |te| self.harvestScopeDecls(te.operand, out),
            .catch_expr => |ce| {
                self.harvestScopeDecls(ce.operand, out);
                self.harvestScopeDecls(ce.body, out);
            },
            .comptime_expr => |ce| self.harvestScopeDecls(ce.expr, out),
            .spread_expr => |se| self.harvestScopeDecls(se.operand, out),
            .lambda => |lm| self.harvestScopeDecls(lm.body, out),
            .fn_decl => |fd| self.harvestScopeDecls(fd.body, out),
            else => {},
        }
    }

    fn checkStructFieldTypes(self: UnknownTypeChecker, sd: *const ast.StructDecl, declared: *std.StringHashMap(void)) void {
        // A generic struct's field types may reference its own type params
        // (`$T`, `$N`, the `..$Ts` pack) — those are IN SCOPE here, so pass them
        // through rather than skipping the whole decl. Skipping silently let a
        // genuinely-undeclared field type (`bad: MissingType`) fall through the
        // type leaf's empty-struct stub and compile (stdlib E3). A value-param
        // position (a `Vector` lane count, a `$N: u32` arg) is still skipped
        // inside `checkTypeNodeForUnknown` / `isValueParamPosition`.
        for (sd.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, sd.type_params, &.{});
    }

    fn checkFnSignatureTypes(self: UnknownTypeChecker, fd: *const ast.FnDecl, declared: *std.StringHashMap(void)) void {
        var in_scope = std.ArrayList(ast.StructTypeParam).empty;
        defer in_scope.deinit(self.alloc);
        var type_vals = std.ArrayList([]const u8).empty;
        defer type_vals.deinit(self.alloc);
        self.checkScope(fd.type_params, fd.params, fd.return_type, fd.body, declared, &in_scope, &type_vals);
    }

    /// Check one function/closure scope: its generic params (`$T`) and value-
    /// `Type` params become in-scope (accumulated onto the parent's, so a nested
    /// closure still sees the outer function's `$T`), its param/return
    /// annotations are checked, then its body is walked. The scope additions are
    /// popped on return.
    fn checkScope(
        self: UnknownTypeChecker,
        type_params: []const ast.StructTypeParam,
        params: []const ast.Param,
        return_type: ?*Node,
        body: *const Node,
        declared: *std.StringHashMap(void),
        in_scope: *std.ArrayList(ast.StructTypeParam),
        type_vals: *std.ArrayList([]const u8),
    ) void {
        const save_s = in_scope.items.len;
        const save_v = type_vals.items.len;
        defer in_scope.shrinkRetainingCapacity(save_s);
        defer type_vals.shrinkRetainingCapacity(save_v);
        for (type_params) |tp| in_scope.append(self.alloc, tp) catch {};
        // Value params declared `: Type` (no `$`) — using one in a type position
        // is the $-prefix-in-cast-position misuse; track them for the tailored hint.
        for (params) |p| {
            if (p.type_expr.data == .type_expr) {
                const cn = p.type_expr.data.type_expr.name;
                if (std.mem.eql(u8, cn, "Type") or std.mem.eql(u8, cn, "type")) {
                    type_vals.append(self.alloc, p.name) catch {};
                }
            }
        }
        for (params) |p| self.checkTypeNodeForUnknown(p.type_expr, declared, in_scope.items, type_vals.items);
        if (return_type) |rt| self.checkTypeNodeForUnknown(rt, declared, in_scope.items, type_vals.items);
        self.walkBodyTypes(body, declared, in_scope, type_vals);
    }

    /// Walk a scope body checking type annotations on local var / const
    /// declarations (and body-local struct fields), descending control flow and
    /// expressions. Nested closure / function literals re-enter via `checkScope`
    /// with their own params added to `in_scope`.
    fn walkBodyTypes(
        self: UnknownTypeChecker,
        node: *const Node,
        declared: *std.StringHashMap(void),
        in_scope: *std.ArrayList(ast.StructTypeParam),
        type_vals: *std.ArrayList([]const u8),
    ) void {
        switch (node.data) {
            .block => |b| for (b.stmts) |s| self.walkBodyTypes(s, declared, in_scope, type_vals),
            .if_expr => |ie| {
                self.walkBodyTypes(ie.condition, declared, in_scope, type_vals);
                self.walkBodyTypes(ie.then_branch, declared, in_scope, type_vals);
                if (ie.else_branch) |e| self.walkBodyTypes(e, declared, in_scope, type_vals);
            },
            .while_expr => |we| {
                self.walkBodyTypes(we.condition, declared, in_scope, type_vals);
                self.walkBodyTypes(we.body, declared, in_scope, type_vals);
            },
            .for_expr => |fe| {
                for (fe.iterables) |it| {
                    self.walkBodyTypes(it.expr, declared, in_scope, type_vals);
                    if (it.range_end) |re| self.walkBodyTypes(re, declared, in_scope, type_vals);
                }
                self.walkBodyTypes(fe.body, declared, in_scope, type_vals);
            },
            .match_expr => |me| {
                self.walkBodyTypes(me.subject, declared, in_scope, type_vals);
                for (me.arms) |arm| self.walkBodyTypes(arm.body, declared, in_scope, type_vals);
            },
            .push_stmt => |ps| {
                self.walkBodyTypes(ps.context_expr, declared, in_scope, type_vals);
                self.walkBodyTypes(ps.body, declared, in_scope, type_vals);
            },
            .defer_stmt => |ds| self.walkBodyTypes(ds.expr, declared, in_scope, type_vals),
            .onfail_stmt => |os| self.walkBodyTypes(os.body, declared, in_scope, type_vals),
            .return_stmt => |r| if (r.value) |v| self.walkBodyTypes(v, declared, in_scope, type_vals),
            .raise_stmt => |rs| self.walkBodyTypes(rs.tag, declared, in_scope, type_vals),
            .assignment => |a| {
                self.walkBodyTypes(a.value, declared, in_scope, type_vals);
                self.walkBodyTypes(a.target, declared, in_scope, type_vals);
            },
            .multi_assign => |ma| for (ma.values) |v| self.walkBodyTypes(v, declared, in_scope, type_vals),
            .destructure_decl => |dd| self.walkBodyTypes(dd.value, declared, in_scope, type_vals),
            .var_decl => |vd| {
                if (vd.type_annotation) |ta| self.checkTypeNodeForUnknown(ta, declared, in_scope.items, type_vals.items);
                if (vd.value) |v| self.walkBodyTypes(v, declared, in_scope, type_vals);
            },
            .const_decl => |cd| {
                if (cd.type_annotation) |ta| self.checkTypeNodeForUnknown(ta, declared, in_scope.items, type_vals.items);
                self.walkBodyTypes(cd.value, declared, in_scope, type_vals);
            },
            .struct_decl => |sd| {
                // A body-local struct's own type params (`$T`) join the enclosing
                // scope's in-scope params (so a local generic struct can name both
                // the outer fn's `$T` and its own); any OTHER bare field type is
                // still a genuinely-undeclared type. Mirrors the top-level
                // `checkStructFieldTypes` close of the generic-struct carveout.
                const save = in_scope.items.len;
                defer in_scope.shrinkRetainingCapacity(save);
                for (sd.type_params) |tp| in_scope.append(self.alloc, tp) catch {};
                for (sd.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, in_scope.items, type_vals.items);
            },
            .call => |c| {
                // `cast(T) x` parses to a `cast` call whose first arg is the
                // target type spelled as an expression. An unknown *literal*
                // target already errors via value resolution; only flag the
                // otherwise-silent value-`Type`-param case here.
                if (c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "cast") and c.args.len == 2) {
                    self.checkCastTarget(c.args[0], in_scope.items, type_vals.items);
                }
                self.walkBodyTypes(c.callee, declared, in_scope, type_vals);
                for (c.args) |a| self.walkBodyTypes(a, declared, in_scope, type_vals);
            },
            .binary_op => |b| {
                self.walkBodyTypes(b.lhs, declared, in_scope, type_vals);
                self.walkBodyTypes(b.rhs, declared, in_scope, type_vals);
            },
            .unary_op => |u| self.walkBodyTypes(u.operand, declared, in_scope, type_vals),
            .field_access => |fa| self.walkBodyTypes(fa.object, declared, in_scope, type_vals),
            .index_expr => |ix| {
                self.walkBodyTypes(ix.object, declared, in_scope, type_vals);
                self.walkBodyTypes(ix.index, declared, in_scope, type_vals);
            },
            .struct_literal => |sl| {
                for (sl.field_inits) |fi| self.walkBodyTypes(fi.value, declared, in_scope, type_vals);
                if (sl.init_block) |ib| self.walkBodyTypes(ib, declared, in_scope, type_vals);
            },
            .array_literal => |al| for (al.elements) |e| self.walkBodyTypes(e, declared, in_scope, type_vals),
            .force_unwrap => |fu| self.walkBodyTypes(fu.operand, declared, in_scope, type_vals),
            .null_coalesce => |nc| {
                self.walkBodyTypes(nc.lhs, declared, in_scope, type_vals);
                self.walkBodyTypes(nc.rhs, declared, in_scope, type_vals);
            },
            .deref_expr => |de| self.walkBodyTypes(de.operand, declared, in_scope, type_vals),
            .try_expr => |te| self.walkBodyTypes(te.operand, declared, in_scope, type_vals),
            .catch_expr => |ce| {
                self.walkBodyTypes(ce.operand, declared, in_scope, type_vals);
                self.walkBodyTypes(ce.body, declared, in_scope, type_vals);
            },
            .comptime_expr => |ce| self.walkBodyTypes(ce.expr, declared, in_scope, type_vals),
            .spread_expr => |se| self.walkBodyTypes(se.operand, declared, in_scope, type_vals),
            .lambda => |lm| self.checkScope(lm.type_params, lm.params, lm.return_type, lm.body, declared, in_scope, type_vals),
            .fn_decl => |fd| self.checkScope(fd.type_params, fd.params, fd.return_type, fd.body, declared, in_scope, type_vals),
            else => {},
        }
    }

    /// A `cast(T)` target naming a value-`Type` parameter (the otherwise-silent
    /// case in cast position) gets the tailored `$T` hint.
    fn checkCastTarget(self: UnknownTypeChecker, arg: *const Node, in_scope: []const ast.StructTypeParam, type_vals: []const []const u8) void {
        const name = switch (arg.data) {
            .identifier => |id| id.name,
            .type_expr => |te| te.name,
            else => return,
        };
        for (in_scope) |tp| if (std.mem.eql(u8, tp.name, name)) return;
        for (type_vals) |tv| {
            if (std.mem.eql(u8, tv, name)) {
                self.diagnostics.addFmt(.err, arg.span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ name, name });
                return;
            }
        }
    }

    /// True when a generic param names a TYPE (so its name may appear in a type
    /// position), false for a VALUE param (`$N: u32`) whose name is a
    /// compile-time integer. A type param is `..$Ts` (the `[]Type` pack), or a
    /// `Type`/protocol-constrained `$T` (`$T: Type`, `$T: Lerpable`). Mirrors the
    /// binder's template-param classification (`lower.zig`); the protocol cases
    /// keep a `$T: SomeProtocol` field type from being wrongly rejected.
    fn isTypeParam(self: UnknownTypeChecker, tp: ast.StructTypeParam) bool {
        if (tp.is_variadic) return true;
        if (tp.constraint.data == .type_expr) {
            const cname = tp.constraint.data.type_expr.name;
            return std.mem.eql(u8, cname, "Type") or
                self.index.protocol_decl_map.contains(cname) or
                self.index.protocol_ast_map.contains(cname);
        }
        return false;
    }

    /// A struct field / fn annotation that names an in-scope generic VALUE param
    /// (`$N: u32`) in a TYPE position is invalid — the name is a compile-time
    /// integer, not a type. The parser marks such a reference `is_generic`
    /// (same as a real type param), so the unknown-type walk would otherwise skip
    /// it and let it reach the `.unresolved` sentinel. Emit the tailored hint; a
    /// genuine type-param reference (or a fresh inline `$R`, not in scope) passes.
    fn reportIfValueParamInTypePosition(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span, in_scope: []const ast.StructTypeParam) void {
        for (in_scope) |tp| {
            if (!std.mem.eql(u8, tp.name, name)) continue;
            if (self.isTypeParam(tp)) return;
            self.diagnostics.addFmt(.err, span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ name, name });
            return;
        }
    }

    /// True when arg `i` of a parameterized type `base(...)` is a VALUE
    /// parameter (a compile-time integer such as a `Vector` lane count or a
    /// generic `$N: u32` arg), not a type. Such a position must be skipped by
    /// the unknown-type walk: a module-const arg (`Vector(N, f32)`) is a value,
    /// not a type name. `Vector`'s arg 0 is always its lane count; a generic
    /// struct template's value-param positions come from its declared params; a
    /// type-RETURNING function (`Make :: ($K: u32, $T: Type) -> Type`) classifies
    /// each param from its constraint, mirroring `instantiateTypeFunction` — so
    /// `Make(N, i64)` (N a module const) is not walked as the type name "N".
    fn isValueParamPosition(self: UnknownTypeChecker, base: []const u8, i: usize) bool {
        if (std.mem.eql(u8, base, "Vector")) return i == 0;
        if (self.index.struct_template_map.get(base)) |tmpl| {
            if (i < tmpl.type_params.len) return !tmpl.type_params[i].is_type_param;
        }
        if (self.index.fn_ast_map.get(base)) |fd| {
            if (i < fd.type_params.len) {
                const tp = fd.type_params[i];
                // A value param is one whose constraint is a non-`Type` type
                // expr (`$K: u32`); a `$T: Type` (or any non-type-expr
                // constraint) is a type param — identical rule to the binder.
                const is_type_param = if (tp.constraint.data == .type_expr)
                    std.mem.eql(u8, tp.constraint.data.type_expr.name, "Type")
                else
                    true;
                return !is_type_param;
            }
        }
        return false;
    }

    /// Recurse a type-annotation node to its leaf names, reporting any unknown.
    fn checkTypeNodeForUnknown(
        self: UnknownTypeChecker,
        node: *const Node,
        declared: *std.StringHashMap(void),
        in_scope: []const ast.StructTypeParam,
        type_vals: []const []const u8,
    ) void {
        switch (node.data) {
            // A `$`-prefixed / struct-param-matched name (`-> $R`, or a field
            // `x: T` naming the struct's own `$T`) is marked `is_generic` by the
            // parser and is normally a valid type-param reference. But the parser
            // marks a struct VALUE param (`$N: u32`) the SAME way, so `x: N` would
            // slip past the unknown-type check and reach the `.unresolved`
            // sentinel (LLVM panic). Catch that one case; a genuine type-param
            // reference still passes.
            .type_expr => |te| if (!te.is_generic)
                self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals, te.is_raw)
            else
                self.reportIfValueParamInTypePosition(te.name, node.span, in_scope),
            .identifier => |id| self.reportIfUnknownType(id.name, node.span, declared, in_scope, type_vals, id.is_raw),
            .pointer_type_expr => |pt| self.checkTypeNodeForUnknown(pt.pointee_type, declared, in_scope, type_vals),
            .many_pointer_type_expr => |mp| self.checkTypeNodeForUnknown(mp.element_type, declared, in_scope, type_vals),
            .slice_type_expr => |st| self.checkTypeNodeForUnknown(st.element_type, declared, in_scope, type_vals),
            .optional_type_expr => |ot| self.checkTypeNodeForUnknown(ot.inner_type, declared, in_scope, type_vals),
            .array_type_expr => |at| self.checkTypeNodeForUnknown(at.element_type, declared, in_scope, type_vals),
            .tuple_type_expr => |tt| for (tt.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, in_scope, type_vals),
            .function_type_expr => |ft| {
                for (ft.param_types) |pt| self.checkTypeNodeForUnknown(pt, declared, in_scope, type_vals);
                if (ft.return_type) |rt| self.checkTypeNodeForUnknown(rt, declared, in_scope, type_vals);
            },
            .closure_type_expr => |ct| {
                // Variadic type-pack closures (`Closure(..$args) -> R`) resolve
                // their projections specially — don't walk them here.
                if (ct.pack_name != null) return;
                for (ct.param_types) |pt| self.checkTypeNodeForUnknown(pt, declared, in_scope, type_vals);
                if (ct.return_type) |rt| self.checkTypeNodeForUnknown(rt, declared, in_scope, type_vals);
            },
            // Builtin constructors (Vector) and generic templates resolve the
            // base name specially; check only the TYPE args. A value-param
            // position (a `Vector` lane count, or a generic `$N: u32` arg) holds
            // a compile-time integer — `Vector(N, f32)` / `Vec(N, f32)` with `N`
            // a module const — not a type name, so it must not be walked as one
            // (it would falsely report "unknown type 'N'"). The lowering
            // resolvers fold the value and emit the precise diagnostic if it
            // isn't a compile-time integer.
            .parameterized_type_expr => |pt| {
                const base = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
                for (pt.args, 0..) |a, i| {
                    if (self.isValueParamPosition(base, i)) continue;
                    self.checkTypeNodeForUnknown(a, declared, in_scope, type_vals);
                }
            },
            else => {},
        }
    }

    fn reportIfUnknownType(
        self: UnknownTypeChecker,
        name: []const u8,
        span: ?ast.Span,
        declared: *std.StringHashMap(void),
        in_scope: []const ast.StructTypeParam,
        type_vals: []const []const u8,
        is_raw: bool,
    ) void {
        // Only bare identifiers are validated. Inline-spelled compound types
        // (`[:0]u8`, `mod.Type`, …) carry non-identifier characters — trust them.
        if (!isIdentLike(name)) return;
        // A backtick raw reference (`` `i2 ``) is the LITERAL name used as a
        // type — explicitly NOT the builtin/reserved spelling — so it must
        // resolve to a `` `i2 ``-declared type, else a normal "unknown type"
        // error. Skip the builtin-name exemption that would otherwise wave a
        // bare `i2` through.
        if (!is_raw and isBuiltinTypeName(name)) return;
        for (in_scope) |tp| {
            if (!std.mem.eql(u8, tp.name, name)) continue;
            // A TYPE param (`$T: Type`, `$T: SomeProtocol`, the `..$Ts` pack)
            // names a type and is valid in this position. A VALUE param
            // (`$N: u32`) is a compile-time integer, NOT a type — accepting it
            // would let the field's type leaf resolve to the `.unresolved`
            // sentinel and panic at LLVM emission. Emit the tailored hint.
            if (self.isTypeParam(tp)) return;
            self.diagnostics.addFmt(.err, span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ name, name });
            return;
        }
        if (declared.contains(name)) return;
        // Registered as a real (non-stub) type — covers imported concrete
        // structs / enums / unions absent from the main-file decl list. A
        // fabricated empty-struct stub (the very thing we're catching) is the
        // sole 0-field-struct case, so it doesn't suppress the diagnostic.
        const sid = self.types.internString(name);
        if (self.types.findByName(sid)) |tid| {
            const info = self.types.get(tid);
            const empty_struct_stub = info == .@"struct" and info.@"struct".fields.len == 0;
            if (!empty_struct_stub) return;
        }
        for (type_vals) |tv| {
            if (std.mem.eql(u8, tv, name)) {
                self.diagnostics.addFmt(.err, span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ name, name });
                return;
            }
        }
        self.diagnostics.addFmt(.err, span, "unknown type '{s}'", .{name});
    }

    /// Reject a value binding (local/global `var` or a parameter) spelled as a
    /// reserved/builtin type name. The parser turns such a spelling
    /// into a `.type_expr` rather than an `.identifier` (`parser.zig`, via
    /// `name_class.Type.fromName`), so the address-of family in `lower.zig`
    /// (`@x`, the autoref `x.method(...)` receiver, a bare `f(x)` at a `*T`
    /// param) never sees a scoped local and falls through to value lowering —
    /// loading the whole aggregate and passing it by value to a `ptr` parameter
    /// (LLVM verifier abort, or a silent mutation-losing copy). Rejecting the
    /// name here, before lowering, keeps the `.identifier`-only address-of paths
    /// correct without any lowering special-case.
    /// `is_raw` is a REQUIRED argument, not a call-site guard: the exemption
    /// lives INSIDE the check so no caller can validate a name without also
    /// honoring the backtick / `#import c` extern exemption. This is what keeps
    /// the check and the exemption from desyncing — the recurring failure of the
    /// earlier attempts, where each site threaded an `if (!is_raw)` guard
    /// separately and one was forgotten.
    fn checkBindingName(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span, is_raw: bool) void {
        if (is_raw) return;
        if (isReservedTypeName(name))
            self.diagnostics.addFmt(.err, span, "'{s}' is a reserved type name and cannot be used as an identifier", .{name});
    }

    /// Reserved-name check for a `::` declaration whose own name binds an
    /// identifier but carries no dedicated `name_span` field — struct / enum /
    /// union / error-set / protocol / runtime-class type decls, ufcs aliases,
    /// and namespaced imports. Each such node begins at its name
    /// token (`createNode(name_start, …)`), so the name's length isolates the
    /// caret onto the name — a single source for the span, no separate stored
    /// field to drift from `node.span`. `is_raw` is REQUIRED, exactly as in
    /// `checkBindingName`: a backtick raw / `#import c` extern name is exempt
    /// by construction.
    fn checkDeclName(self: UnknownTypeChecker, node: *const Node, name: []const u8, is_raw: bool) void {
        const span = ast.Span{ .start = node.span.start, .end = node.span.start + @as(u32, @intCast(name.len)) };
        self.checkBindingName(name, span, is_raw);
    }
};

/// A binding name collides with a reserved/builtin type name exactly when the
/// parser would classify the same spelling as a type. `name_class.Type.fromName`
/// is that classifier (`parser.zig` uses it to choose `.type_expr` over
/// `.identifier`), so deferring to it ties the rejection to the parser's set and
/// keeps the two from drifting: the named builtins (`bool`, `string`, `void`,
/// `f32`, `f64`, `usize`, `isize`, `Any`) and the `[iu]N` arbitrary-width ints
/// over sx's supported 1–64 range. A bare value name (`s`, `buf`, `index`,
/// `self`) is not a type spelling and is left alone.
fn isReservedTypeName(name: []const u8) bool {
    return name_class.Type.fromName(name) != null;
}

fn isBuiltinTypeName(name: []const u8) bool {
    if (TypeResolver.resolvePrimitive(name) != null) return true;
    // Arbitrary-width integers / floats: u1, i7, u128, f16, f80, …
    if (name.len >= 2 and (name[0] == 'u' or name[0] == 'i' or name[0] == 'f')) {
        var all_digits = true;
        for (name[1..]) |c| {
            if (!std.ascii.isDigit(c)) {
                all_digits = false;
                break;
            }
        }
        if (all_digits) return true;
    }
    const extra = [_][]const u8{ "Type", "type", "int", "float", "Self", "self", "any", "noreturn", "usize", "isize", "comptime_int", "comptime_float" };
    for (extra) |e| if (std.mem.eql(u8, name, e)) return true;
    return false;
}

/// True when a `const_decl`'s value introduces a TYPE name — a type declaration
/// (`struct`/`enum`/`union`/`error`) or a type-expression alias (`Alias :: u32`,
/// `Ptr :: *u8`, `Cb :: (i32) -> i32`, …). Only these belong in the declared-
/// type-name set; a value const (`NotAType :: 123`) does NOT declare a type and
/// must stay subject to the unknown-type check.
///
/// `.identifier` / `.call` aliases (`B :: A`, `Vec3 :: Vec(3, f32)`) are
/// deliberately NOT matched here: the scan registers the type-valued ones into
/// `ProgramIndex.type_alias_map` / the `TypeTable` (both queried separately), so
/// a value-RHS alias is correctly left out and flagged, while a type-RHS alias
/// is still covered by the canonical facts.
fn constValueIntroducesType(value: *const Node) bool {
    return switch (value.data) {
        .struct_decl, .enum_decl, .union_decl, .error_set_decl => true,
        .type_expr,
        .pointer_type_expr,
        .many_pointer_type_expr,
        .slice_type_expr,
        .optional_type_expr,
        .array_type_expr,
        .function_type_expr,
        .closure_type_expr,
        .tuple_type_expr,
        .parameterized_type_expr,
        => true,
        else => false,
    };
}

fn isIdentLike(name: []const u8) bool {
    if (name.len == 0) return false;
    if (!(std.ascii.isAlphabetic(name[0]) or name[0] == '_')) return false;
    for (name) |c| {
        if (!(std.ascii.isAlphanumeric(c) or c == '_')) return false;
    }
    return true;
}
