fix(lower): source-aware initial scan registration for identifier-RHS aliases [stdlib E1.5 attempt-2]

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).
This commit is contained in:
agra
2026-06-07 21:12:33 +03:00
parent 2d34993586
commit d2eb4c2af4
6 changed files with 77 additions and 14 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
forward A (u64=300): 300
direct B (u64=300): 300
ns.width(): 8

View File

@@ -934,18 +934,26 @@ pub const Lowering = struct {
} }
self.putTypeAlias(self.current_source_file, cd.name, target_ty); self.putTypeAlias(self.current_source_file, cd.name, target_ty);
} else if (cd.value.data == .identifier) { } else if (cd.value.data == .identifier) {
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide; // Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide.
// Chase through type_alias_map, then look up named types // SOURCE-AWARE (E1.5). Resolve the RHS `B` AS SEEN FROM this
// in the table. Forward references resolve lazily because // alias's OWN source via `selectNominalLeaf` (E1's source-
// the .identifier branch of resolveTypeArg also consults // keyed nominal leaf), NEVER the global `type_alias_map` /
// type_alias_map at use time. // global `findByName` (last-wins across modules). Only the
const rhs_name = cd.value.data.identifier.name; // `.resolved` outcome is written; `.pending` (B is itself a
if (self.program_index.type_alias_map.get(rhs_name)) |chained| { // forward alias not resolved yet), `.undeclared`, and
self.putTypeAlias(self.current_source_file, cd.name, chained); // `.not_visible` (a same-name B authored only by a namespaced
} else { // import) leave A UNWRITTEN so the source-aware
const name_id = self.module.types.internString(rhs_name); // `resolveForwardIdentifierAliases` fixpoint re-tries A once
if (self.module.types.findByName(name_id)) |tid| { // the local B registers. A GLOBAL selection here would bind A
self.putTypeAlias(self.current_source_file, cd.name, tid); // 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; if (cd.value.data != .identifier) continue;
const src = decl.source_file orelse self.main_file orelse continue; const src = decl.source_file orelse self.main_file orelse continue;
if (self.aliasResolvedInSource(src, cd.name)) continue; if (self.aliasResolvedInSource(src, cd.name)) continue;
const rhs_name = cd.value.data.identifier.name; const rhs = cd.value.data.identifier;
switch (self.selectNominalLeaf(rhs_name, src, false)) { // 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| { .resolved => |tid| {
self.putTypeAlias(decl.source_file, cd.name, tid); self.putTypeAlias(decl.source_file, cd.name, tid);
progressed = true; progressed = true;