fix: reject non-type expression in type position instead of fabricating {} (issue 0189)
Two type-resolution paths silently resolved a non-type AST node in type
position to a zero-field `{}` struct that reached codegen with no
diagnostic:
- a dotted `type_expr` / field-access (`g.a`, `g` a runtime value) whose
prefix is not a namespace alias
- an `error_type_expr` (`!Name`) whose `Name` is not a declared error set
Now both reject loudly:
- `resolveTypeWithBindings` (lower.zig): "expected a type, found a value
'<name>' in type position" + `.unresolved`
- `checkTypeNodeForUnknown` (semantic_diagnostics.zig): validates a named
`!E` against the declared error-set names — "unknown error set
'<name>'" / "expected an error set after '!', found type '<name>'".
A bare `!` (void channel) and a declared `!E` in return position stay
valid; namespace-qualified types (`pkg.Type`) are unaffected.
Regression: examples/diagnostics/1195-diagnostics-non-type-in-type-position.
This commit is contained in:
@@ -1068,6 +1068,29 @@ pub const Lowering = struct {
|
||||
self.setCurrentSourceFile(saved);
|
||||
return ty;
|
||||
}
|
||||
// A dotted `type_expr` whose prefix is NOT a namespace alias
|
||||
// is the only remaining qualified form — and sx has no
|
||||
// `Type.NestedType` access, so this is a VALUE field access
|
||||
// (`g.a` where `g` is a value) sitting in a type position.
|
||||
// Without this guard `resolveNominalLeaf("g.a")` would
|
||||
// fabricate a zero-field empty-struct stub (`{}`) and ship it
|
||||
// to codegen as a real type (issue 0189) — a silent-default
|
||||
// miscompile. Reject loudly and poison with `.unresolved`.
|
||||
// A genuinely registered dotted type (none today, but a
|
||||
// forward-declared stub could exist) is still honored before
|
||||
// we reject, so we never reject a name that resolves to a
|
||||
// real type.
|
||||
if (!self.aliasDeclaredAnywhere(te.name[0..dot])) {
|
||||
const sid = self.module.types.internString(te.name);
|
||||
if (self.module.types.findByName(sid)) |tid| {
|
||||
const info = self.module.types.get(tid);
|
||||
const is_empty_stub = info == .@"struct" and info.@"struct".fields.len == 0;
|
||||
if (!is_empty_stub) return tid;
|
||||
}
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "expected a type, found a value '{s}' in type position", .{te.name});
|
||||
return .unresolved;
|
||||
}
|
||||
}
|
||||
return self.resolveNominalLeaf(te.name, te.is_raw, node.span);
|
||||
},
|
||||
|
||||
@@ -45,6 +45,13 @@ pub const UnknownTypeChecker = struct {
|
||||
types: *TypeTable,
|
||||
index: *ProgramIndex,
|
||||
main_file: ?[]const u8,
|
||||
/// Declared error-set names (`E :: error { ... }`) gathered across every
|
||||
/// compiled module + nested scope. Populated in `run`; consulted by the
|
||||
/// `.error_type_expr` arm of `checkTypeNodeForUnknown` to tell a valid
|
||||
/// `!E` (E a declared set) apart from an undeclared name or a value name
|
||||
/// used after `!` in type position (both of which silently fabricate a
|
||||
/// zero-field `{}` stub — issue 0189). `null` only before `run` populates it.
|
||||
error_sets: ?*const std.StringHashMap(void) = null,
|
||||
|
||||
pub fn run(self: UnknownTypeChecker, decls: []const *const Node) void {
|
||||
// Reserved-type-name binding diagnostic: rejects any
|
||||
@@ -65,6 +72,14 @@ pub const UnknownTypeChecker = struct {
|
||||
var declared = std.StringHashMap(void).init(self.alloc);
|
||||
defer declared.deinit();
|
||||
self.collectDeclaredTypeNames(decls, &declared);
|
||||
// Declared error-set names — every module + nested scope. Used by the
|
||||
// `.error_type_expr` arm to validate `!E` (issue 0189). Collected
|
||||
// unfiltered (imported sets count: `g :: () -> i64 !LibErr` is valid).
|
||||
var error_sets = std.StringHashMap(void).init(self.alloc);
|
||||
defer error_sets.deinit();
|
||||
for (decls) |decl| self.collectErrorSetNames(decl, &error_sets);
|
||||
var checker = self;
|
||||
checker.error_sets = &error_sets;
|
||||
const saved_file = self.diagnostics.current_source_file;
|
||||
defer self.diagnostics.current_source_file = saved_file;
|
||||
for (decls) |decl| {
|
||||
@@ -77,11 +92,11 @@ pub const UnknownTypeChecker = struct {
|
||||
// previous phase left behind (issue 0122).
|
||||
if (decl.source_file) |sf| self.diagnostics.current_source_file = sf;
|
||||
switch (decl.data) {
|
||||
.fn_decl => self.checkFnSignatureTypes(&decl.data.fn_decl, &declared),
|
||||
.struct_decl => |sd| self.checkStructFieldTypes(&sd, &declared),
|
||||
.fn_decl => checker.checkFnSignatureTypes(&decl.data.fn_decl, &declared),
|
||||
.struct_decl => |sd| checker.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),
|
||||
.fn_decl => checker.checkFnSignatureTypes(&cd.value.data.fn_decl, &declared),
|
||||
.struct_decl => |sd| checker.checkStructFieldTypes(&sd, &declared),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
@@ -89,6 +104,34 @@ pub const UnknownTypeChecker = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gather declared error-set names (`E :: error { ... }`) into `out`, from a
|
||||
/// top-level decl and every nested scope (fn / closure bodies, local type
|
||||
/// decls). A top-level error set parses as a bare `.error_set_decl`; a
|
||||
/// local one is a `.const_decl` whose value is an `.error_set_decl`. Walking
|
||||
/// every module (not just the main file) keeps an imported `!LibErr` valid.
|
||||
fn collectErrorSetNames(self: UnknownTypeChecker, node: *const Node, out: *std.StringHashMap(void)) void {
|
||||
switch (node.data) {
|
||||
.error_set_decl => |esd| out.put(esd.name, {}) catch {},
|
||||
.const_decl => |cd| {
|
||||
if (cd.value.data == .error_set_decl)
|
||||
out.put(cd.value.data.error_set_decl.name, {}) catch {};
|
||||
self.collectErrorSetNames(cd.value, out);
|
||||
},
|
||||
.fn_decl => |fd| self.collectErrorSetNames(fd.body, out),
|
||||
.block => |b| for (b.stmts) |s| self.collectErrorSetNames(s, out),
|
||||
.if_expr => |ie| {
|
||||
self.collectErrorSetNames(ie.then_branch, out);
|
||||
if (ie.else_branch) |e| self.collectErrorSetNames(e, out);
|
||||
},
|
||||
.while_expr => |we| self.collectErrorSetNames(we.body, out),
|
||||
.for_expr => |fe| self.collectErrorSetNames(fe.body, out),
|
||||
.match_expr => |me| for (me.arms) |arm| self.collectErrorSetNames(arm.body, out),
|
||||
.push_stmt => |ps| self.collectErrorSetNames(ps.body, out),
|
||||
.lambda => |lm| self.collectErrorSetNames(lm.body, out),
|
||||
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
|
||||
@@ -814,10 +857,49 @@ pub const UnknownTypeChecker = struct {
|
||||
self.checkTypeNodeForUnknown(a, declared, in_scope, type_vals);
|
||||
}
|
||||
},
|
||||
// `!E` (named failable channel) in type position. A bare `!`
|
||||
// (name == null) is the inferred/void channel and is always valid.
|
||||
// A named `!E` is valid ONLY when `E` is a declared error set;
|
||||
// otherwise the lowering path silently fabricates a zero-field
|
||||
// `{}` stub (issue 0189), so reject it here with a precise
|
||||
// diagnostic — "unknown error set" for an undeclared name, "expected
|
||||
// an error set" for a name that resolves to a non-error-set type or
|
||||
// a value.
|
||||
.error_type_expr => |ete| if (ete.name) |name|
|
||||
self.reportIfNotErrorSet(name, node.span),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the `E` in an `!E` type. `E` must be a declared error set.
|
||||
/// Distinguishes three failure shapes so the user gets an actionable
|
||||
/// message: an undeclared name (`unknown error set`), and a declared name
|
||||
/// that is NOT an error set — a value or a non-error-set type (`expected an
|
||||
/// error set`). Mirrors the silent-fabrication guard for `g.a` in
|
||||
/// `resolveTypeWithBindings` (issue 0189): never let a non-error-set name
|
||||
/// after `!` reach the lowering stub.
|
||||
fn reportIfNotErrorSet(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span) void {
|
||||
// Inline-spelled / qualified spellings (`mod.E`) carry non-identifier
|
||||
// characters — trust them, matching `reportIfUnknownType`.
|
||||
if (!isIdentLike(name)) return;
|
||||
const sets = self.error_sets orelse return;
|
||||
if (sets.contains(name)) return;
|
||||
// A name that names a real (non-error-set) TYPE — a struct/enum/union,
|
||||
// a builtin, or a fabricated stub — is a type-in-error-position misuse.
|
||||
if (isBuiltinTypeName(name)) {
|
||||
self.diagnostics.addFmt(.err, span, "expected an error set after '!', found type '{s}'", .{name});
|
||||
return;
|
||||
}
|
||||
const sid = self.types.internString(name);
|
||||
if (self.types.findByName(sid)) |_| {
|
||||
self.diagnostics.addFmt(.err, span, "expected an error set after '!', found type '{s}'", .{name});
|
||||
return;
|
||||
}
|
||||
// Otherwise the name is undeclared (or names a value): no error-set
|
||||
// author anywhere. Either way it is not a usable error set.
|
||||
self.diagnostics.addFmt(.err, span, "unknown error set '{s}'", .{name});
|
||||
}
|
||||
|
||||
fn reportIfUnknownType(
|
||||
self: UnknownTypeChecker,
|
||||
name: []const u8,
|
||||
|
||||
Reference in New Issue
Block a user