fix(lower): pin nested-leaf source to the SELECTED const's author — close F1 R1 [stdlib E2 attempt-5]

A same-name expression const read from another module folded its nested
leaves (`M` inside `K :: M + 1`) from the CALLER's source, not the source
that authored the selected const. A unique imported `K` became ambiguous
when the reading module also flat-imported a different same-name `M`.

`selectModuleConst` now returns the author SOURCE alongside the const info
(`SelectedConst`), and the fold/lower of a selected const's RHS pins
`current_source_file` to that author for the duration (`pinConstAuthorSource`)
— so `K :: M + 1` defined in `a.sx` always folds `M` against `a.sx`,
coherently whether `K` is read as a runtime value or used as an array
dimension. Each recursion level pins to its own selected author's source.

Single-author programs pin to the source they were already in → byte-
identical (499 prior examples unchanged). Genuine ambiguity at the read
site (0760) is still caught before any pin.

Regression: examples/0762-modules-same-name-const-leaf-author-pin
(`a.sx M::1; K::M+1`, `b.sx M::10`, main flat-imports both, reads K as
value AND `[K]u8` dimension → val=2 len=2). Fail-before on 8518b66
(`'M' is ambiguous` / "array dimension must be a compile-time integer
constant"), pass-after.
This commit is contained in:
agra
2026-06-08 07:02:59 +03:00
parent 8518b66cec
commit 4666fb1941
7 changed files with 111 additions and 22 deletions

View File

@@ -1427,16 +1427,18 @@ pub const Lowering = struct {
// same pass-2 before this). E2/F2: copy the SOURCE-AWARE author's
// value (own-wins), and reject a ≥2-flat ambiguity loudly.
if (self.program_index.module_const_map.get(id.name)) |ci_global| {
const ci = switch (self.selectModuleConst(id.name)) {
.resolved => |c| c,
.none => ci_global,
const sel: SelectedConst = switch (self.selectModuleConst(id.name)) {
.resolved => |s| s,
.none => .{ .info = ci_global, .source = null },
.ambiguous => {
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name});
break :blk null;
},
};
if (self.constExprValue(ci.value, var_ty)) |cv| break :blk cv;
const author_pin = self.pinConstAuthorSource(sel.source);
defer author_pin.unpin();
if (self.constExprValue(sel.info.value, var_ty)) |cv| break :blk cv;
}
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name });
@@ -3962,7 +3964,7 @@ pub const Lowering = struct {
// silent pick. `.none` after a visible name is the registration-
// only author (no per-source partition) — emit its global value.
switch (self.selectModuleConst(id.name)) {
.resolved => |ci| break :blk self.emitModuleConst(ci),
.resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source),
.ambiguous => {
if (self.diagnostics) |d|
d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name});
@@ -3971,7 +3973,7 @@ pub const Lowering = struct {
// "unresolved" cascade from `emitError`.
break :blk self.emitPlaceholder(id.name);
},
.none => break :blk self.emitModuleConst(ci_global),
.none => break :blk self.emitModuleConst(ci_global, null),
}
}
// Check if it's a function name — produce function pointer reference
@@ -13467,10 +13469,12 @@ pub const Lowering = struct {
fn foldSourceConstFloat(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?f64 {
if (constFoldFrameContains(frame, name)) return null;
return switch (self.selectModuleConst(name)) {
.resolved => |ci| {
if (!program_index_mod.isCountableConstType(&self.module.types, ci.ty)) return null;
.resolved => |sel| {
if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null;
var f = ConstFoldFrame{ .name = name, .parent = frame };
return program_index_mod.evalConstFloatExpr(ci.value, SourceConstCtx{ .lowering = self, .frame = &f });
const restore = self.pinConstAuthorSource(sel.source);
defer restore.unpin();
return program_index_mod.evalConstFloatExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f });
},
.ambiguous, .none => null,
};
@@ -13492,10 +13496,12 @@ pub const Lowering = struct {
fn sourceConstIsFloatTyped(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) bool {
if (constFoldFrameContains(frame, name)) return false;
return switch (self.selectModuleConst(name)) {
.resolved => |ci| {
if (program_index_mod.isFloatConstType(ci.ty)) return true;
.resolved => |sel| {
if (program_index_mod.isFloatConstType(sel.info.ty)) return true;
var f = ConstFoldFrame{ .name = name, .parent = frame };
return program_index_mod.isFloatValuedExpr(ci.value, SourceConstCtx{ .lowering = self, .frame = &f });
const restore = self.pinConstAuthorSource(sel.source);
defer restore.unpin();
return program_index_mod.isFloatValuedExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f });
},
.ambiguous, .none => false,
};
@@ -13530,15 +13536,28 @@ pub const Lowering = struct {
fn foldSourceConstInt(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?i64 {
if (constFoldFrameContains(frame, name)) return null;
return switch (self.selectModuleConst(name)) {
.resolved => |ci| {
if (!program_index_mod.isCountableConstType(&self.module.types, ci.ty)) return null;
.resolved => |sel| {
if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null;
var f = ConstFoldFrame{ .name = name, .parent = frame };
return program_index_mod.evalConstIntExpr(ci.value, SourceConstCtx{ .lowering = self, .frame = &f });
const restore = self.pinConstAuthorSource(sel.source);
defer restore.unpin();
return program_index_mod.evalConstIntExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f });
},
.ambiguous, .none => null,
};
}
/// A selected module const plus the SOURCE that authored it. `source` pins the
/// context in which the const's RHS leaves must be folded (F1): a same-name
/// `K :: M + 1` selected from author `a.sx` folds its nested `M` against `a.sx`,
/// not against whichever module read `K`. `source` is null only on the
/// fully-unwired fallback (no source partition at all), where the RHS resolves
/// through the global registration context unchanged.
const SelectedConst = struct {
info: program_index_mod.ModuleConstInfo,
source: ?[]const u8,
};
/// The source-aware module-const author of `name` from the querying module
/// (E2/F2) — the value-const analogue of `selectNominalLeaf` (types) and
/// `selectPlainCallableAuthor` (functions). Selects over the ONE graph-walk
@@ -13556,29 +13575,29 @@ pub const Lowering = struct {
/// at all) falls open to the global registration, byte-identical to the legacy
/// reader for the registration / comptime-host path.
const ConstAuthor = union(enum) {
resolved: program_index_mod.ModuleConstInfo,
resolved: SelectedConst,
ambiguous,
none,
};
fn selectModuleConst(self: *Lowering, name: []const u8) ConstAuthor {
const from = self.current_source_file orelse self.main_file orelse {
if (self.program_index.module_const_map.get(name)) |ci| return .{ .resolved = ci };
if (self.program_index.module_const_map.get(name)) |ci| return .{ .resolved = .{ .info = ci, .source = null } };
return .none;
};
var res = self.resolver();
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
defer if (set.flat.len > 0) self.alloc.free(set.flat);
if (set.own) |o| if (self.sourceModuleConst(o.source, name)) |ci| return .{ .resolved = ci };
var the_one: ?program_index_mod.ModuleConstInfo = null;
if (set.own) |o| if (self.sourceModuleConst(o.source, name)) |ci| return .{ .resolved = .{ .info = ci, .source = o.source } };
var the_one: ?SelectedConst = null;
var count: usize = 0;
for (set.flat) |fa| {
const ci = self.sourceModuleConst(fa.source, name) orelse continue;
count += 1;
if (count >= 2) return .ambiguous;
the_one = ci;
the_one = .{ .info = ci, .source = fa.source };
}
if (the_one) |ci| return .{ .resolved = ci };
if (the_one) |sc| return .{ .resolved = sc };
return .none;
}
@@ -13589,6 +13608,31 @@ pub const Lowering = struct {
return inner.get(name);
}
/// Saved `current_source_file` for a const-author pin; `unpin()` restores it.
const ConstSourcePin = struct {
lowering: *Lowering,
saved: ?[]const u8,
active: bool,
fn unpin(self: ConstSourcePin) void {
if (self.active) self.lowering.setCurrentSourceFile(self.saved);
}
};
/// Pin `current_source_file` to a SELECTED const's AUTHOR source while its RHS
/// is folded / lowered, so nested same-name leaves resolve in the author's
/// visibility context (F1): `K :: M + 1` selected from `a.sx` always folds `M`
/// against `a.sx`, regardless of which module read `K`. A null author (the
/// fully-unwired fallback) leaves the context untouched. Single-author programs
/// pin to the source they were already in → byte-identical.
fn pinConstAuthorSource(self: *Lowering, source: ?[]const u8) ConstSourcePin {
if (source) |s| {
const saved = self.current_source_file;
self.setCurrentSourceFile(s);
return .{ .lowering = self, .saved = saved, .active = true };
}
return .{ .lowering = self, .saved = self.current_source_file, .active = false };
}
/// Resolve a type node, checking type_bindings first for generic type params.
pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId {
// Pack-index in a type position: `$<pack>[<lit>]` resolves to the
@@ -16123,7 +16167,14 @@ pub const Lowering = struct {
};
}
fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref {
fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo, author_source: ?[]const u8) Ref {
// F1: a const read from another module folds/lowers its RHS in the
// AUTHOR's visibility context, so a same-name leaf (`K :: M + 1` selected
// from `a.sx`) resolves `M` against `a.sx` — not against the reading
// module, which may flat-import a different same-name `M`. Single-author /
// own-read consts pin to the source they were already in → byte-identical.
const author_pin = self.pinConstAuthorSource(author_source);
defer author_pin.unpin();
// An integer-typed const whose initializer is a compile-time integer —
// an int literal/expression, OR an INTEGRAL float that `typedConstInitFits`
// accepted under the unified narrowing rule — materializes as its folded