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

@@ -0,0 +1,18 @@
// A top-level VALUE constant name used in a type position is rejected. Without
// the fix the unknown-type pass added every `const_decl` name to its declared-
// type set, so a value const (`NotAType :: 123`) satisfied the check and the
// type resolver's unknown-name fallback then fabricated an empty struct — the
// program ran and printed `NotAType{}`. Now only consts whose value introduces a
// type (declarations / type-expression aliases) count as type names.
// Regression (issue 0068).
// Expected: a clean "unknown type 'NotAType'" error at the annotation; exit 1.
#import "modules/std.sx";
NotAType :: 123;
main :: () -> s32 {
v: NotAType = ---;
print("value = {}\n", v);
return 0;
}

View File

@@ -0,0 +1,5 @@
error: unknown type 'NotAType'
--> examples/1117-diagnostics-value-const-as-type-rejected.sx:15:8
|
15 | v: NotAType = ---;
| ^^^^^^^^

View File

@@ -0,0 +1,99 @@
# 0068 — top-level value const used as a type silently yields an empty struct
> **RESOLVED** (2026-06-02).
> **Root cause:** the A2.4 unknown-type pass (`semantic_diagnostics`) inherited the
> issue-0064 behavior of adding 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{}`.
> **Fix:** `collectDeclaredTypeNames` and `harvestScopeDecls` now add a const name
> only when its value INTRODUCES a type — gated on a new `constValueIntroducesType`
> (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 test:** `examples/1117-diagnostics-value-const-as-type-rejected.sx`
> (exit 1). Issue-0064 regressions 11111116 + the `0115` aliases stay green.
> Suite 352/0.
## Symptom
A top-level value constant name is accepted in a type position and silently
resolves to a fabricated empty struct.
Observed:
```text
value = NotAType{}
```
Expected: a user-facing diagnostic rejecting `NotAType` as a type, with no
fabricated empty-struct type.
## Reproduction
```sx
#import "modules/std.sx";
NotAType :: 123;
main :: () -> s32 {
v: NotAType = ---;
print("value = {}\n", v);
return 0;
}
```
Run:
```sh
./zig-out/bin/sx run .sx-tmp/probe-top-level-value-const-as-type.sx
```
The repro is standalone; the inline source above is sufficient to recreate the
scratch file under `.sx-tmp/`.
## Investigation prompt
Fix issue 0068: a value constant name must not satisfy the unknown-type
diagnostic pass or resolve as a fabricated type when used in a type position.
Suspected area:
- `src/ir/semantic_diagnostics.zig`, especially
`UnknownTypeChecker.collectDeclaredTypeNames` and `harvestScopeDecls`.
- The moved issue-0064 pass currently adds every `const_decl` name to the
`declared` set. That preserves old behavior, but it means a value const like
`NotAType :: 123;` suppresses `reportIfUnknownType`, then the later type
resolver's unknown-name fallback interns an empty struct named `NotAType`.
- Related fallback: `TypeResolver.resolveNamed` / `type_bridge.resolveAstType`
still create empty struct stubs for unknown names in paths that the diagnostic
pass is supposed to reject before lowering reaches codegen.
Likely fix:
- Change `collectDeclaredTypeNames` / `harvestScopeDecls` so only declarations
that actually introduce type-position names are added: struct / enum / union /
error declarations, type aliases, generic templates, protocols, foreign
classes, and local type declarations.
- Do not add arbitrary value const names to the type-name set.
- Preserve valid type alias behavior such as `Alias :: u32;` and local
type-declaration behavior.
- Keep the pass querying canonical facts (`ProgramIndex`, `TypeResolver`, and
`TypeTable`) rather than reintroducing a parallel top-level truth table.
Verification:
- Add a focused diagnostics example in the `11xx` block for the repro above,
expecting exit 1 and a clear diagnostic.
- Keep issue-0064 regressions green (`1111` through `1115`) and keep existing
alias/type-declaration examples green.
- Run:
```sh
zig build
zig build test
bash tests/run_examples.sh
```
Expected result: `NotAType :: 123; v: NotAType` is rejected with a diagnostic,
valid aliases and type declarations still resolve, and the full suite passes.

View File

@@ -65,7 +65,11 @@ pub const UnknownTypeChecker = struct {
for (decls) |decl| { for (decls) |decl| {
switch (decl.data) { switch (decl.data) {
.const_decl => |cd| { .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); if (cd.value.data == .fn_decl) self.harvestScopeDecls(cd.value.data.fn_decl.body, out);
}, },
.struct_decl => |sd| out.put(sd.name, {}) catch {}, .struct_decl => |sd| out.put(sd.name, {}) catch {},
@@ -127,7 +131,11 @@ pub const UnknownTypeChecker = struct {
.destructure_decl => |dd| self.harvestScopeDecls(dd.value, out), .destructure_decl => |dd| self.harvestScopeDecls(dd.value, out),
.var_decl => |vd| if (vd.value) |v| self.harvestScopeDecls(v, out), .var_decl => |vd| if (vd.value) |v| self.harvestScopeDecls(v, out),
.const_decl => |cd| { .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); self.harvestScopeDecls(cd.value, out);
}, },
.struct_decl => |sd| out.put(sd.name, {}) catch {}, .struct_decl => |sd| out.put(sd.name, {}) catch {},
@@ -428,6 +436,35 @@ fn isBuiltinTypeName(name: []const u8) bool {
return false; 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 { fn isIdentLike(name: []const u8) bool {
if (name.len == 0) return false; if (name.len == 0) return false;
if (!(std.ascii.isAlphabetic(name[0]) or name[0] == '_')) return false; if (!(std.ascii.isAlphabetic(name[0]) or name[0] == '_')) return false;