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:
18
examples/1117-diagnostics-value-const-as-type-rejected.sx
Normal file
18
examples/1117-diagnostics-value-const-as-type-rejected.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: unknown type 'NotAType'
|
||||||
|
--> examples/1117-diagnostics-value-const-as-type-rejected.sx:15:8
|
||||||
|
|
|
||||||
|
15 | v: NotAType = ---;
|
||||||
|
| ^^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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 1111–1116 + 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.
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user