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 (issue 0064), 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 (issues 0076, 0077): 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` (foreign 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 (issues 0076, 0077): 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 (issue 0077). for (decls) |decl| self.checkBindingNames(decl); // Unknown-type diagnostic (issue 0064): 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); for (decls) |decl| { if (self.main_file) |mf| { if (decl.source_file) |sf| { if (!std.mem.eql(u8, sf, mf)) continue; } } 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 (issues 0076, 0077). 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 (issue 0077). 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. ── .var_decl => |vd| { if (!vd.is_raw) self.checkBindingName(vd.name, vd.name_span); if (vd.value) |v| self.checkBindingNames(v); }, .destructure_decl => |dd| { for (dd.names, dd.name_spans) |n, sp| self.checkBindingName(n, sp); self.checkBindingNames(dd.value); }, .fn_decl => |fd| { self.checkParamNames(fd.params); self.checkBindingNames(fd.body); }, .lambda => |lm| { self.checkParamNames(lm.params); self.checkBindingNames(lm.body); }, .param => |p| { if (!p.is_raw) self.checkBindingName(p.name, p.name_span); if (p.default_expr) |de| self.checkBindingNames(de); }, .if_expr => |ie| { if (ie.binding_name) |bn| self.checkBindingName(bn, ie.binding_span); 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); self.checkBindingNames(we.condition); self.checkBindingNames(we.body); }, .for_expr => |fe| { if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, fe.capture_span); if (fe.index_name) |idx| self.checkBindingName(idx, fe.index_span); self.checkBindingNames(fe.iterable); if (fe.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); if (arm.pattern) |p| self.checkBindingNames(p); self.checkBindingNames(arm.body); } }, .match_arm => |arm| { if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span); if (arm.pattern) |p| self.checkBindingNames(p); self.checkBindingNames(arm.body); }, .catch_expr => |ce| { if (ce.binding) |b| self.checkBindingName(b, ce.binding_span); self.checkBindingNames(ce.operand); self.checkBindingNames(ce.body); }, .onfail_stmt => |os| { if (os.binding) |b| self.checkBindingName(b, os.binding_span); self.checkBindingNames(os.body); }, // impl / protocol-default / foreign-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| for (pd.methods) |m| { if (m.default_body) |body| { for (m.param_names, m.param_name_spans) |pn, sp| self.checkBindingName(pn, sp); self.checkBindingNames(body); } }, .foreign_class_decl => |fcd| for (fcd.members) |member| switch (member) { .method => |m| if (m.body) |body| { for (m.param_names, m.param_name_spans) |pn, sp| self.checkBindingName(pn, sp); 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 (issue 0077). .namespace_decl => |nd| for (nd.decls) |d| self.checkBindingNames(d), .const_decl => |cd| self.checkBindingNames(cd.value), .struct_decl => |sd| { 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), // ── Leaves & pure type-expression nodes: no binding sites below. ── // Type-expression subtrees carry only type names (no value // bindings); enum / union / error-set declarations carry only field // types + comptime constants. 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, .enum_decl, .union_decl, .error_set_decl, .import_decl, .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, .compiler_expr, .foreign_expr, .library_decl, .framework_decl, .function_type_expr, .closure_type_expr, .tuple_type_expr, .ufcs_alias, .c_import_decl, => {}, } } /// 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 (`` (`s2: T) ``) or a `#import c` foreign // param is exempt from the reserved-type-name rule (issue 0089). if (!p.is_raw) self.checkBindingName(p.name, p.name_span); 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 foreign-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 (issue 0068). 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.foreign_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| { self.harvestScopeDecls(fe.iterable, out); if (fe.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 (issue 0068). 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 { // Generic struct fields reference the struct's own type params ($T) — // resolved at instantiation, not here. if (sd.type_params.len != 0) return; for (sd.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, &.{}, &.{}); } 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 issue-0064 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| { self.walkBodyTypes(fe.iterable, declared, in_scope, type_vals); if (fe.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| if (sd.type_params.len == 0) { 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 /// issue-0064 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 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, s64)` (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 name (`-> $R`) introduces/references a generic type // param inline — always valid in a type position. .type_expr => |te| if (!te.is_generic) self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals), .identifier => |id| self.reportIfUnknownType(id.name, node.span, declared, in_scope, type_vals), .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, ) void { // Only bare identifiers are validated. Inline-spelled compound types // (`[:0]u8`, `mod.Type`, …) carry non-identifier characters — trust them. if (!isIdentLike(name)) return; if (isBuiltinTypeName(name)) return; for (in_scope) |tp| if (std.mem.eql(u8, tp.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 (issue 0076). 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. fn checkBindingName(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span) void { if (isReservedTypeName(name)) self.diagnostics.addFmt(.err, span, "'{s}' is a reserved type name and cannot be used as an identifier", .{name}); } }; /// 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 `[su]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, s7, u128, f16, f80, … if (name.len >= 2 and (name[0] == 'u' or name[0] == 's' 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 :: (s32) -> s32`, …). 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 (issue 0068). /// /// `.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; }