Files
sx/src/ir/semantic_diagnostics.zig
agra 116af2359e lang: multi-iterable for loops — drop ':', add '..=', open ranges, arrow bodies
The for header is now a comma-separated list of iterables with a
positional capture group and no ':' separator:

    for xs (x) { }                    // collection
    for 0..n (i) { }                  // range (end exclusive)
    for 1..=5 (a) { }                 // ..= inclusive end
    for xs, 0.. (x, i) { }            // index idiom (replaces (x, i))
    for xs, ys (x, y) { }             // parallel (zip) iteration
    for xs (x) => sum += x;           // arrow body (full statement)

First-iterable-wins: the first iterable's length drives the loop and
must be bounded; the other positions follow by their own cursors (a
non-first range's end is not consulted or evaluated; a shorter
non-first collection is read past its length on mismatch). The old
single-iterable index capture is replaced by the trailing open range.

Capture/call disambiguation is positional: the paren group immediately
before '{' or '=>' is the capture, every earlier top-level group is a
call. 'for zip(a, b) (x, y)' calls zip; 'for f(n) { }' reads (n) as
the capture and errors with a parenthesize/add-capture hint. The old
':' form errors with a migration hint.

Lowering is unified across forms: one cursor slot per position (ranges
start at their start, collections at 0), all advanced together, the
first position's bound terminating. inline for keeps the single
bounded comptime range.

Migrated the full corpus (examples, library modules, issue repros,
in-source test strings). New coverage: examples/0050 (the full feature
surface) and examples/1149-1155 (seven diagnostic faces). specs.md For
Loop section + grammar rewritten; readme teaser updated.
2026-06-10 20:30:55 +03:00

951 lines
51 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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` (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: 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);
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. 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
// `s2 :: (…) {…}` (free fn or struct/impl method) is rejected,
// exactly like `s2 := …`. Backtick (`` `s2 :: … ``) and
// `#import c` foreign 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 / 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| {
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);
}
}
},
.foreign_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),
// ── 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,
.compiler_expr,
.foreign_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 (`` (`s2: T) ``) or a `#import c` foreign
// 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 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.
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| {
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, 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 / 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 (`` `s2 ``) is the LITERAL name used as a
// type — explicitly NOT the builtin/reserved spelling — so it must
// resolve to a `` `s2 ``-declared type, else a normal "unknown type"
// error. Skip the builtin-name exemption that would otherwise wave a
// bare `s2` 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` foreign 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 / foreign-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` foreign 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 `[su]N` arbitrary-width ints
/// over sx's supported 164 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.
///
/// `.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;
}