From 877014578ed283fa4aace43b4b2ea65a94581a61 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 16:33:38 +0300 Subject: [PATCH] fix(ir): value const used as a type must not satisfy unknown-type check (issue 0068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...iagnostics-value-const-as-type-rejected.sx | 18 ++++ ...gnostics-value-const-as-type-rejected.exit | 1 + ...ostics-value-const-as-type-rejected.stderr | 5 + ...ostics-value-const-as-type-rejected.stdout | 1 + ...st-used-as-type-suppresses-unknown-type.md | 99 +++++++++++++++++++ src/ir/semantic_diagnostics.zig | 41 +++++++- 6 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 examples/1117-diagnostics-value-const-as-type-rejected.sx create mode 100644 examples/expected/1117-diagnostics-value-const-as-type-rejected.exit create mode 100644 examples/expected/1117-diagnostics-value-const-as-type-rejected.stderr create mode 100644 examples/expected/1117-diagnostics-value-const-as-type-rejected.stdout create mode 100644 issues/0068-value-const-used-as-type-suppresses-unknown-type.md diff --git a/examples/1117-diagnostics-value-const-as-type-rejected.sx b/examples/1117-diagnostics-value-const-as-type-rejected.sx new file mode 100644 index 0000000..43f97bb --- /dev/null +++ b/examples/1117-diagnostics-value-const-as-type-rejected.sx @@ -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; +} diff --git a/examples/expected/1117-diagnostics-value-const-as-type-rejected.exit b/examples/expected/1117-diagnostics-value-const-as-type-rejected.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1117-diagnostics-value-const-as-type-rejected.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1117-diagnostics-value-const-as-type-rejected.stderr b/examples/expected/1117-diagnostics-value-const-as-type-rejected.stderr new file mode 100644 index 0000000..50014a2 --- /dev/null +++ b/examples/expected/1117-diagnostics-value-const-as-type-rejected.stderr @@ -0,0 +1,5 @@ +error: unknown type 'NotAType' + --> examples/1117-diagnostics-value-const-as-type-rejected.sx:15:8 + | +15 | v: NotAType = ---; + | ^^^^^^^^ diff --git a/examples/expected/1117-diagnostics-value-const-as-type-rejected.stdout b/examples/expected/1117-diagnostics-value-const-as-type-rejected.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1117-diagnostics-value-const-as-type-rejected.stdout @@ -0,0 +1 @@ + diff --git a/issues/0068-value-const-used-as-type-suppresses-unknown-type.md b/issues/0068-value-const-used-as-type-suppresses-unknown-type.md new file mode 100644 index 0000000..1340c55 --- /dev/null +++ b/issues/0068-value-const-used-as-type-suppresses-unknown-type.md @@ -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. diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 1ef39ba..d5a8d6a 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -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;