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:
agra
2026-06-25 20:35:02 +03:00
parent f52e16a3fc
commit 45e69ac1bb
7 changed files with 197 additions and 5 deletions

View File

@@ -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);
},