Reserved type-name spellings (s1, s2, u8, …) can now be used as value identifiers two ways, resolving issue 0089: 1. Backtick raw identifier: a leading backtick (`s2) lexes to an .identifier token carrying a new Token.is_raw flag, with the backtick excluded from the text. A raw identifier is never type-classified — the parser skips Type.fromName for it — so it is always a value identifier. The flag threads to VarDecl.is_raw / Param.is_raw at binding sites, and the reserved-type-name check (UnknownTypeChecker) skips raw bindings. Because the token tag stays .identifier, the escape works in every position (local, global, param, field, fn name, struct member, later reference) with no per-site parser change. 2. #import c exemption: c_import.zig synthesizes foreign decls with Param.is_raw = true, so generated C param names that collide with reserved type names (s1, s2) import unedited. A bare reserved-name binding in sx still errors (issue 0076 preserved): the is_raw-gated skip only fires for backtick / foreign names, and a raw binding's address-of / autoref lowering stays correct because every occurrence is an .identifier, never a .type_expr. Tests: examples/0151 (backtick, every position), examples/1220 (foreign exemption, compiled+run), lexer unit tests. 1119 (bare-binding rejection) stays green. specs.md + readme.md updated.
812 lines
42 KiB
Zig
812 lines
42 KiB
Zig
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;
|
||
}
|