fix(diagnostics): reject reserved type-name bindings in every module (issue 0077)

The issue-0076 reserved-type-name binding diagnostic only ran over main-file
decls, so an imported module (or the stdlib) could still declare `s2 := ...`
and reach lowering, where the address-of family loads the whole aggregate and
passes it by value to a `ptr` param — LLVM verifier abort.

Extend coverage to every compiled module: a dedicated `checkBindingNames` walk
(in semantic_diagnostics.zig) visits every var/`:=`/typed-local binding name and
function/lambda/struct-method parameter at any depth, with NO main-file filter,
descending the `namespace_decl` that a `mod :: #import` wraps so imported-module
decls are reached. It tracks each module's source_file (save/restore per node)
so the diagnostic renders against the imported module's text. Rejection still
defers to the parser's `Type.fromName` classifier; the unknown-type check (0064)
stays main-file-only. No lowering special-case; `.identifier`-only address-of
paths are unchanged.

Stdlib audit: the only reserved-name bindings under library/ were two `u1`
locals in ui/renderer.sx (UV coords) — renamed to u_min/u_max/v_min/v_max.

Regression test: examples/1120-diagnostics-imported-reserved-type-name.sx (+
companion mod.sx) — an imported `s2 := ...` now emits the clean diagnostic at
the import's declaration site (exit 1), not an LLVM abort.

Resolves issues 0076 (coverage extension) and 0077.
This commit is contained in:
agra
2026-06-03 19:32:49 +03:00
parent f49a49cd07
commit df6e830bec
9 changed files with 301 additions and 30 deletions

View File

@@ -11,15 +11,20 @@ const TypeTable = types.TypeTable;
const ProgramIndex = program_index_mod.ProgramIndex;
const TypeResolver = type_resolver.TypeResolver;
/// Declaration-name / type-position diagnostic pass. Two checks, both over the
/// main file's decls, before lowering:
/// 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.
/// 2. Reserved-type-name binding (issue 0076): a value binding (local/global
/// `var` or a parameter) spelled as a reserved/builtin type name. See
/// `isReservedTypeName`.
/// 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
@@ -42,6 +47,21 @@ pub const UnknownTypeChecker = struct {
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);
@@ -54,7 +74,6 @@ pub const UnknownTypeChecker = struct {
switch (decl.data) {
.fn_decl => self.checkFnSignatureTypes(&decl.data.fn_decl, &declared),
.struct_decl => |sd| self.checkStructFieldTypes(&sd, &declared),
.var_decl => |vd| self.checkBindingName(vd.name, decl.span),
.const_decl => |cd| switch (cd.value.data) {
.fn_decl => self.checkFnSignatureTypes(&cd.value.data.fn_decl, &declared),
.struct_decl => |sd| self.checkStructFieldTypes(&sd, &declared),
@@ -65,6 +84,108 @@ pub const UnknownTypeChecker = struct {
}
}
/// Reserved-type-name binding walk (issues 0076, 0077). Visits every binding
/// site reachable from `node` — `var` / `:=` / typed-local declarations and
/// function / lambda / struct-method parameters, at any nesting depth — and
/// rejects each name that collides with a reserved/builtin type name. Walks
/// into expressions too, so a lambda nested in a call arg / struct literal is
/// reached. Deliberately filter-free (every 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) {
.var_decl => |vd| {
self.checkBindingName(vd.name, node.span);
if (vd.value) |v| self.checkBindingNames(v);
},
.fn_decl => |fd| {
for (fd.params) |p| self.checkBindingName(p.name, p.name_span);
self.checkBindingNames(fd.body);
},
.lambda => |lm| {
for (lm.params) |p| self.checkBindingName(p.name, p.name_span);
self.checkBindingNames(lm.body);
},
.const_decl => |cd| self.checkBindingNames(cd.value),
// 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),
.struct_decl => |sd| for (sd.methods) |m| self.checkBindingNames(m),
.block => |b| for (b.stmts) |s| self.checkBindingNames(s),
.if_expr => |ie| {
self.checkBindingNames(ie.condition);
self.checkBindingNames(ie.then_branch);
if (ie.else_branch) |e| self.checkBindingNames(e);
},
.while_expr => |we| {
self.checkBindingNames(we.condition);
self.checkBindingNames(we.body);
},
.for_expr => |fe| {
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| self.checkBindingNames(arm.body);
},
.push_stmt => |ps| {
self.checkBindingNames(ps.context_expr);
self.checkBindingNames(ps.body);
},
.defer_stmt => |ds| self.checkBindingNames(ds.expr),
.onfail_stmt => |os| self.checkBindingNames(os.body),
.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.values) |v| self.checkBindingNames(v),
.destructure_decl => |dd| self.checkBindingNames(dd.value),
.call => |c| {
self.checkBindingNames(c.callee);
for (c.args) |a| self.checkBindingNames(a);
},
.binary_op => |b| {
self.checkBindingNames(b.lhs);
self.checkBindingNames(b.rhs);
},
.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);
},
.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),
.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),
.catch_expr => |ce| {
self.checkBindingNames(ce.operand);
self.checkBindingNames(ce.body);
},
.comptime_expr => |ce| self.checkBindingNames(ce.expr),
.spread_expr => |se| self.checkBindingNames(se.operand),
else => {},
}
}
/// 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-
@@ -233,7 +354,6 @@ pub const UnknownTypeChecker = struct {
}
}
}
for (params) |p| self.checkBindingName(p.name, p.name_span);
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);
@@ -285,7 +405,6 @@ pub const UnknownTypeChecker = struct {
.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| {
self.checkBindingName(vd.name, node.span);
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);
},