fix(ir): value const used as a type must not satisfy unknown-type check (issue 0068)

The A2.4 unknown-type pass (semantic_diagnostics) added EVERY const_decl name to
its declared-type-name set. A value const (`NotAType :: 123`) thus satisfied
reportIfUnknownType, so `v: NotAType` was not flagged; lowering then hit
TypeResolver.resolveNamed's empty-struct-stub fallback and fabricated
`NotAType{}` (the program ran, printing it).

Fix: collectDeclaredTypeNames and harvestScopeDecls now gate the const-name-add
on a new constValueIntroducesType — true only when the value introduces a type
(declarations: struct/enum/union/error; type-expression aliases: type_expr,
pointer/many-pointer/slice/optional/array/function/closure/tuple, parameterized).
`.identifier` / `.call` aliases are intentionally excluded: the scan registers
the type-valued ones into ProgramIndex.type_alias_map / the TypeTable (both
queried separately by the pass), so a value-RHS alias is correctly left out and
flagged, while a type-RHS alias stays covered by the canonical facts.

Regression: examples/1117-diagnostics-value-const-as-type-rejected.sx (exit 1).
Issue-0064 regressions 1111-1116 and the 0115 aliases stay green. Gate: zig
build, zig build test, run_examples 352/0.
This commit is contained in:
agra
2026-06-02 16:33:38 +03:00
parent 8ff24472c9
commit 877014578e
6 changed files with 163 additions and 2 deletions

View File

@@ -65,7 +65,11 @@ pub const UnknownTypeChecker = struct {
for (decls) |decl| {
switch (decl.data) {
.const_decl => |cd| {
out.put(cd.name, {}) catch {};
// 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 (issue 0068).
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 {},
@@ -127,7 +131,11 @@ pub const UnknownTypeChecker = struct {
.destructure_decl => |dd| self.harvestScopeDecls(dd.value, out),
.var_decl => |vd| if (vd.value) |v| self.harvestScopeDecls(v, out),
.const_decl => |cd| {
out.put(cd.name, {}) catch {};
// Local type decl (`T :: struct/enum/union/error/alias`) — add
// its name; a local VALUE const (`x :: 5`) does not declare a
// type (issue 0068). 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 {},
@@ -428,6 +436,35 @@ fn isBuiltinTypeName(name: []const u8) bool {
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 (issue 0068).
///
/// `.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;