From 49a383df6dac1d4c3dc626a2526f1fc24eb33a40 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 16:59:20 +0300 Subject: [PATCH] fix(ir): resolve forward identifier type aliases in scanDecls (issue 0069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scanDecls' `.identifier` alias branch registered `A :: B` into ProgramIndex.type_alias_map only when `B` was already known (in type_alias_map or the TypeTable). A forward target declared later (`MyChain :: MyInt; MyInt :: s32;`) was never present during the single forward scan, so the alias name went unregistered and the A2.4 unknown-type pass — which treats type_alias_map keys as declared types — flagged its uses as `unknown type 'MyChain'`. Add a fixpoint post-pass `resolveForwardIdentifierAliases` at the end of scanDecls that re-resolves identifier-RHS aliases until no progress, after every top-level name has been seen. A value const is never an `.identifier` node, and an alias whose target is a value const still misses both lookups, so issue 0068's value-const rejection is preserved. Regression: examples/0132-types-forward-type-alias.sx (forward alias + forward chain). Gate: zig build, zig build test, run_examples.sh -> 353/0. --- examples/0132-types-forward-type-alias.sx | 24 ++++ .../0132-types-forward-type-alias.exit | 1 + .../0132-types-forward-type-alias.stderr | 1 + .../0132-types-forward-type-alias.stdout | 3 + ...d-identifier-type-alias-falsely-unknown.md | 114 ++++++++++++++++++ src/ir/lower.zig | 37 ++++++ 6 files changed, 180 insertions(+) create mode 100644 examples/0132-types-forward-type-alias.sx create mode 100644 examples/expected/0132-types-forward-type-alias.exit create mode 100644 examples/expected/0132-types-forward-type-alias.stderr create mode 100644 examples/expected/0132-types-forward-type-alias.stdout create mode 100644 issues/0069-forward-identifier-type-alias-falsely-unknown.md diff --git a/examples/0132-types-forward-type-alias.sx b/examples/0132-types-forward-type-alias.sx new file mode 100644 index 0000000..8f17709 --- /dev/null +++ b/examples/0132-types-forward-type-alias.sx @@ -0,0 +1,24 @@ +// Forward identifier type alias — an alias whose target is declared LATER +// in the file resolves the same as an ordered one. `MyChain :: MyInt;` +// appears before `MyInt :: s32;`, yet `MyChain` resolves to `s32` and a +// forward chain (`A :: B; B :: C; C :: u8;`) converges too. +// Regression (issue 0069): the scan only registered identifier aliases whose +// target was already known, so a forward alias was falsely flagged +// `unknown type`. Now a fixpoint pass over the scanned decls resolves them. +#import "modules/std.sx"; + +MyChain :: MyInt; +MyInt :: s32; + +A :: B; +B :: C; +C :: u8; + +main :: () -> s32 { + v: MyChain = 7; + n: A = 3; + print("chain s32: {}\n", size_of(MyChain)); + print("forward u8: {}\n", size_of(A)); + print("v + n: {}\n", v + cast(s32) n); + return v; +} diff --git a/examples/expected/0132-types-forward-type-alias.exit b/examples/expected/0132-types-forward-type-alias.exit new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/examples/expected/0132-types-forward-type-alias.exit @@ -0,0 +1 @@ +7 diff --git a/examples/expected/0132-types-forward-type-alias.stderr b/examples/expected/0132-types-forward-type-alias.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0132-types-forward-type-alias.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0132-types-forward-type-alias.stdout b/examples/expected/0132-types-forward-type-alias.stdout new file mode 100644 index 0000000..63e1822 --- /dev/null +++ b/examples/expected/0132-types-forward-type-alias.stdout @@ -0,0 +1,3 @@ +chain s32: 4 +forward u8: 1 +v + n: 10 diff --git a/issues/0069-forward-identifier-type-alias-falsely-unknown.md b/issues/0069-forward-identifier-type-alias-falsely-unknown.md new file mode 100644 index 0000000..0c4e5ae --- /dev/null +++ b/issues/0069-forward-identifier-type-alias-falsely-unknown.md @@ -0,0 +1,114 @@ +# 0069 — forward identifier type alias is falsely rejected by unknown-type pass + +> **RESOLVED.** Root cause: `Lowering.scanDecls`' `.identifier` alias branch only +> registered `A :: B` into `ProgramIndex.type_alias_map` when `B` was already +> known (in `type_alias_map` or the `TypeTable`). A forward target declared later +> (`MyChain :: MyInt; MyInt :: s32;`) was never present during the single forward +> scan, so the alias name went unregistered and the A2.4 unknown-type pass — which +> treats `type_alias_map` keys as declared types — flagged its uses. +> Fix: added a fixpoint post-pass `resolveForwardIdentifierAliases` at the end of +> `scanDecls` that re-resolves identifier-RHS aliases until no progress, after every +> top-level name has been seen. A value const is never an `.identifier` node and an +> alias whose target is a value const still misses both lookups, so issue 0068's +> value-const rejection is preserved. Regression: +> `examples/0132-types-forward-type-alias.sx`. + +## Symptom + +A forward-referenced identifier type alias is rejected as an unknown type, even +though the same alias chain works when ordered after its target. + +Observed: `MyChain` is diagnosed as an unknown type. + +Expected: `MyChain :: MyInt; MyInt :: s32;` should resolve `MyChain` to `s32` +when used in a type annotation, matching the existing ordered-chain behavior +(`MyInt :: s32; MyChain :: MyInt;`). + +## Reproduction + +```sx +MyChain :: MyInt; +MyInt :: s32; + +main :: () -> s32 { + v: MyChain = 7; + return v; +} +``` + +Run: + +```sh +./zig-out/bin/sx run .sx-tmp/probe-0068-forward-alias.sx +``` + +Observed output: + +```text +error: unknown type 'MyChain' + --> .sx-tmp/probe-0068-forward-alias.sx:7:8 + | + 7 | v: MyChain = 7; + | ^^^^^^^ +``` + +The repro is standalone; the inline source above is sufficient to recreate the +scratch file under `.sx-tmp/`. + +## Investigation prompt + +Fix issue 0069: a forward-referenced identifier type alias must not be falsely +rejected by the A2.4 unknown-type diagnostic pass. + +Context: +- This surfaced while re-reviewing `8770145`, the issue-0068 fix. That fix + correctly stopped arbitrary value consts (`NotAType :: 123`) from satisfying + the unknown-type check. +- Ordered identifier aliases still work: `MyInt :: s32; MyChain :: MyInt;`. +- `.call` type aliases still work: `Vec3 :: Vec(3, f32);` and + `Foo :: Complex(u32);`. +- The failing shape is specifically a forward identifier alias: + `MyChain :: MyInt; MyInt :: s32;`. + +Suspected area: +- `src/ir/lower.zig`, `Lowering.scanDecls`, especially the `.identifier` alias + branch for `const_decl` values. It only inserts `cd.name` into + `ProgramIndex.type_alias_map` if the RHS is already in `type_alias_map` or + already registered in the `TypeTable`. A forward target is not present yet, so + the alias name is never recorded. +- `src/ir/semantic_diagnostics.zig`, + `UnknownTypeChecker.collectDeclaredTypeNames` / `reportIfUnknownType`. After + issue 0068, `.identifier` aliases are intentionally excluded from + `constValueIntroducesType` and are supposed to be covered by canonical facts + (`ProgramIndex.type_alias_map` / `TypeTable`). Because the forward alias never + reaches those facts, the checker flags the alias as unknown. + +Likely fix: +- Do not reintroduce the issue-0068 bug by adding all `.identifier` const names + to the declared-type set. +- Instead, make identifier aliases converge through canonical alias facts even + when the RHS is declared later. A small two-pass alias registration/resolution + in `scanDecls`, or an explicit pending-alias graph that resolves after all + top-level declarations are scanned, would keep `ProgramIndex.type_alias_map` + authoritative without accepting value constants. +- Preserve value const rejection: `NotAType :: 123; v: NotAType` must continue + to emit `unknown type 'NotAType'`. +- Preserve ordered/chained aliases and type-returning call aliases: + `examples/0116-types-type-alias-size-align.sx`, + `examples/0201-generics-generic-struct.sx`, and + `examples/1117-diagnostics-value-const-as-type-rejected.sx`. + +Verification: +- Add a focused regression for the repro above, likely in the `01xx` types block + because the desired behavior is successful alias resolution. +- Run the new regression and the existing alias/diagnostic guards: + +```sh +zig build +zig build test +bash tests/run_examples.sh +``` + +Expected result: the forward alias program compiles/runs (returning `7`), the +issue-0068 value-const case still fails with a diagnostic, and the full suite +passes. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 7e0b316..932b134 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1253,6 +1253,43 @@ pub const Lowering = struct { else => {}, } } + self.resolveForwardIdentifierAliases(decls); + } + + /// Resolve identifier-RHS type aliases whose target is declared LATER in the + /// file. The forward scan above only registers an alias (`A :: B`) when `B` + /// is already in `type_alias_map` / the `TypeTable`; a forward target isn't + /// yet present, so `A` is left unregistered and its uses get falsely flagged + /// as an unknown type (issue 0069). Re-resolve to a fixpoint now that every + /// top-level name has been seen, so `A :: B; B :: s32;` converges the same as + /// the ordered `B :: s32; A :: B;`. A value const is never an `.identifier` + /// node (`NotAType :: 123` is an int literal), and an alias whose target is a + /// value const still misses both lookups, so neither this pass nor issue 0068 + /// can register a non-type name. + fn resolveForwardIdentifierAliases(self: *Lowering, decls: []const *const Node) void { + var progressed = true; + while (progressed) { + progressed = false; + for (decls) |decl| { + const cd = switch (decl.data) { + .const_decl => |c| c, + else => continue, + }; + if (cd.value.data != .identifier) continue; + if (self.program_index.type_alias_map.contains(cd.name)) continue; + const rhs_name = cd.value.data.identifier.name; + if (self.program_index.type_alias_map.get(rhs_name)) |chained| { + self.program_index.type_alias_map.put(cd.name, chained) catch {}; + progressed = true; + } else { + const name_id = self.module.types.internString(rhs_name); + if (self.module.types.findByName(name_id)) |tid| { + self.program_index.type_alias_map.put(cd.name, tid) catch {}; + progressed = true; + } + } + } + } } /// Try to convert an array literal's elements into a compile-time ConstantValue.aggregate.