From d2eb4c2af4be935c9c2c1d789d0da87ac638d1dc Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 7 Jun 2026 21:12:33 +0300 Subject: [PATCH] fix(lower): source-aware initial scan registration for identifier-RHS aliases [stdlib E1.5 attempt-2] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E1.5 attempt-1 made the forward-alias FIXPOINT source-aware but left the EARLIER path — the `scanDecls` identifier-RHS alias branch — resolving the RHS through the GLOBAL `type_alias_map` / global `findByName` (last-wins across modules). When a namespaced import is scanned BEFORE a forward alias `A :: B; B :: u64;`, dep's same-name `B :: u8` already sits in the global map, so the early scan bound `A` to dep's `u8` and the per-source fixpoint guard (`aliasResolvedInSource`) then skipped `A` — re-opening 0105 one layer down (reviewer R1). Cut the scan registration over to `selectNominalLeaf(rhs, src, is_raw)`, resolving `B` AS SEEN FROM the alias's OWN source. Only the `.resolved` outcome is written via the unified `putTypeAlias`; `.pending` / `.undeclared` / `.not_visible` leave `A` UNWRITTEN so the source-aware fixpoint re-tries it once the local `B` registers. No raw `type_alias_map.put` / global `findByName` selection reintroduced (E1 no-drift invariant). resolver.zig untouched (single graph-walk invariant). Also thread the backtick raw flag (`identifier.is_raw`) into BOTH the scan registration and the fixpoint `selectNominalLeaf` calls, so a raw-RHS alias (`` RawAlias :: `s2 ``) resolves to the nominal `` `s2 `` author, not the builtin `s2` spelling (fixes 0154 under the new scan path; closes the same latent hardcode in the fixpoint). Regression: examples/0751-modules-forward-alias-ns-before — the reviewer's exact ordering (ns import with `B :: u8` BEFORE `A :: B; B :: u64;`). Fails on 2d34993 (`forward A` = 44, dep's u8) and passes after (= 300, local u64). 0750 + 0132/0133 + the full suite stay byte-identical (488/0). --- .../0751-modules-forward-alias-ns-before.sx | 36 +++++++++++++++++ .../dep.sx | 11 ++++++ .../0751-modules-forward-alias-ns-before.exit | 1 + ...751-modules-forward-alias-ns-before.stderr | 1 + ...751-modules-forward-alias-ns-before.stdout | 3 ++ src/ir/lower.zig | 39 ++++++++++++------- 6 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 examples/0751-modules-forward-alias-ns-before.sx create mode 100644 examples/0751-modules-forward-alias-ns-before/dep.sx create mode 100644 examples/expected/0751-modules-forward-alias-ns-before.exit create mode 100644 examples/expected/0751-modules-forward-alias-ns-before.stderr create mode 100644 examples/expected/0751-modules-forward-alias-ns-before.stdout diff --git a/examples/0751-modules-forward-alias-ns-before.sx b/examples/0751-modules-forward-alias-ns-before.sx new file mode 100644 index 0000000..c72bf4b --- /dev/null +++ b/examples/0751-modules-forward-alias-ns-before.sx @@ -0,0 +1,36 @@ +// Source-aware forward-alias fixpoint — the INITIAL scan registration (E1.5). +// Sibling of 0750, but the namespaced import is placed BEFORE the forward alias: +// +// ns :: #import ".../dep.sx"; // authors a same-name `B :: u8` +// A :: B; // forward alias +// B :: u64; // its LOCAL target, declared later +// +// Because `ns` is scanned first, dep's `B :: u8` is already in the global +// `type_alias_map` when `A :: B` is scanned. The pre-E1.5-attempt-2 `scanDecls` +// identifier-RHS branch resolved the RHS through that GLOBAL map and bound `A` to +// dep's `u8` right there; the per-source fixpoint guard then saw `A` already +// resolved and skipped it, so the source-aware leaf never corrected it (`A` came +// out `u8`, truncating 300 -> 44). The source-aware scan registration resolves +// `B` AS SEEN FROM main's source: dep's `B` is namespaced-only (not bare-visible) +// and main's `B` is not registered yet, so `A` is left UNWRITTEN and the fixpoint +// binds it to the local `B :: u64` once that registers. +// +// Observable: a runtime 300 coerced into an `A`-typed slot round-trips as 300 +// when `A` is `u64` (correct) and truncates to 44 when `A` is wrongly `u8`. +// Regression (stdlib E1.5). +#import "modules/std.sx"; + +ns :: #import "0751-modules-forward-alias-ns-before/dep.sx"; + +A :: B; +B :: u64; + +main :: () -> s32 { + n : s64 = 300; + a : A = xx n; + b : B = xx n; + print("forward A (u64=300): {}\n", cast(s64) a); + print("direct B (u64=300): {}\n", cast(s64) b); + print("ns.width(): {}\n", ns.width()); + return 0; +} diff --git a/examples/0751-modules-forward-alias-ns-before/dep.sx b/examples/0751-modules-forward-alias-ns-before/dep.sx new file mode 100644 index 0000000..da12c43 --- /dev/null +++ b/examples/0751-modules-forward-alias-ns-before/dep.sx @@ -0,0 +1,11 @@ +// Namespaced helper module. It authors a top-level type alias `B` whose spelling +// collides with the importer's own `B`. Because the import is NAMESPACED +// (`ns :: #import`), `dep.B` is NOT flat-visible to the importer — but its alias +// write still lands in the global `type_alias_map`, which the source-aware +// forward-alias scan registration must ignore even when this module is scanned +// (and so populates the global map) BEFORE the importer's own `A :: B`. +B :: u8; + +width :: () -> s32 { + return 8; +} diff --git a/examples/expected/0751-modules-forward-alias-ns-before.exit b/examples/expected/0751-modules-forward-alias-ns-before.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0751-modules-forward-alias-ns-before.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0751-modules-forward-alias-ns-before.stderr b/examples/expected/0751-modules-forward-alias-ns-before.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0751-modules-forward-alias-ns-before.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0751-modules-forward-alias-ns-before.stdout b/examples/expected/0751-modules-forward-alias-ns-before.stdout new file mode 100644 index 0000000..7b44149 --- /dev/null +++ b/examples/expected/0751-modules-forward-alias-ns-before.stdout @@ -0,0 +1,3 @@ +forward A (u64=300): 300 +direct B (u64=300): 300 +ns.width(): 8 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index ed0897c..d872352 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -934,18 +934,26 @@ pub const Lowering = struct { } self.putTypeAlias(self.current_source_file, cd.name, target_ty); } else if (cd.value.data == .identifier) { - // Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide; - // Chase through type_alias_map, then look up named types - // in the table. Forward references resolve lazily because - // the .identifier branch of resolveTypeArg also consults - // type_alias_map at use time. - const rhs_name = cd.value.data.identifier.name; - if (self.program_index.type_alias_map.get(rhs_name)) |chained| { - self.putTypeAlias(self.current_source_file, cd.name, chained); - } else { - const name_id = self.module.types.internString(rhs_name); - if (self.module.types.findByName(name_id)) |tid| { - self.putTypeAlias(self.current_source_file, cd.name, tid); + // Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide. + // SOURCE-AWARE (E1.5). Resolve the RHS `B` AS SEEN FROM this + // alias's OWN source via `selectNominalLeaf` (E1's source- + // keyed nominal leaf), NEVER the global `type_alias_map` / + // global `findByName` (last-wins across modules). Only the + // `.resolved` outcome is written; `.pending` (B is itself a + // forward alias not resolved yet), `.undeclared`, and + // `.not_visible` (a same-name B authored only by a namespaced + // import) leave A UNWRITTEN so the source-aware + // `resolveForwardIdentifierAliases` fixpoint re-tries A once + // the local B registers. A GLOBAL selection here would bind A + // to a namespaced same-name B, and the per-source fixpoint + // guard (`aliasResolvedInSource`) would then SKIP A — leaving + // the wrong global TypeId and re-opening 0105 one layer down + // (R1, E1.5). Same unified `putTypeAlias` writer (no-drift). + const rhs = cd.value.data.identifier; + if (self.current_source_file orelse self.main_file) |from| { + switch (self.selectNominalLeaf(rhs.name, from, rhs.is_raw)) { + .resolved => |tid| self.putTypeAlias(self.current_source_file, cd.name, tid), + .pending, .undeclared, .not_visible => {}, } } } @@ -1396,8 +1404,11 @@ pub const Lowering = struct { if (cd.value.data != .identifier) continue; const src = decl.source_file orelse self.main_file orelse continue; if (self.aliasResolvedInSource(src, cd.name)) continue; - const rhs_name = cd.value.data.identifier.name; - switch (self.selectNominalLeaf(rhs_name, src, false)) { + const rhs = cd.value.data.identifier; + // Pass the backtick raw flag so a forward alias whose RHS is a raw + // identifier (`` RawAlias :: `s2 ``, target declared later) resolves + // to the nominal `` `s2 `` author, not the builtin `s2` spelling. + switch (self.selectNominalLeaf(rhs.name, src, rhs.is_raw)) { .resolved => |tid| { self.putTypeAlias(decl.source_file, cd.name, tid); progressed = true;