fix(diagnostics): reject reserved/builtin type names used as identifiers (issue 0076)
A value binding (local/global `var` or a parameter) spelled as a reserved/builtin type name parses as a `.type_expr` rather than an `.identifier` (parser.zig, via `Type.fromName`), so the address-of family in lower.zig never saw a scoped local and mis-lowered it — loading the aggregate and passing it by value to a `ptr` parameter (LLVM verifier abort, or a silent `*self`-mutation-losing copy). Add a declaration-site diagnostic in semantic_diagnostics.zig (`UnknownTypeChecker.checkBindingName`): reject any parameter name or `var` binding name (`:=` / typed-local / global forms) whose spelling collides with a reserved type name. `isReservedTypeName` defers to the parser's own classifier (`types.Type.fromName`) so the rejected set never drifts from the set that would parse as a type — the named builtins (bool/string/void/f32/f64/usize/isize/Any) and `[su]N` over sx's 1-64 range. Bare value names (`s`, `self`, `index`) are untouched. No lowering special-case; the `.identifier`-only address-of paths are correct once type-shaped names can never be bound. The rejected attempt-1 `bareVarName` approach was never landed. Tests: - 0125-types-type-named-var-rejected: `:=` form (s2) rejected (repurposed from the old test that asserted the now-illegal behavior). - 1119-diagnostics-reserved-type-name-as-identifier: parameter (u8), typed-local (s64, bool), `:=` (string) forms rejected. - 0135-types-self-streaming-nonreserved: positive — `*self` streaming with non-reserved names accumulates correctly via both call styles. - 0904-optionals: renamed incidental locals s1/s2 -> filled/empty.
This commit is contained in:
@@ -2,6 +2,7 @@ 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");
|
||||
|
||||
@@ -10,10 +11,17 @@ const TypeTable = types.TypeTable;
|
||||
const ProgramIndex = program_index_mod.ProgramIndex;
|
||||
const TypeResolver = type_resolver.TypeResolver;
|
||||
|
||||
/// Unknown-type diagnostic pass (issue 0064), extracted from `Lowering`
|
||||
/// (architecture phase A2.4). Rejects an identifier used in a type position
|
||||
/// that names no declared type, primitive, or in-scope generic type parameter.
|
||||
/// Without it, `TypeResolver.resolveNamed`'s empty-struct-stub fallback silently
|
||||
/// Declaration-name / type-position diagnostic pass. Two checks, both over the
|
||||
/// main file's decls, 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`.
|
||||
///
|
||||
/// 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;
|
||||
@@ -46,6 +54,7 @@ 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),
|
||||
@@ -224,6 +233,7 @@ 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);
|
||||
@@ -275,6 +285,7 @@ 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);
|
||||
},
|
||||
@@ -416,8 +427,35 @@ pub const UnknownTypeChecker = struct {
|
||||
}
|
||||
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, …
|
||||
|
||||
Reference in New Issue
Block a user