fix(lower): source-aware value-const resolution (own-wins / ambiguous) — close F2 [stdlib E2 attempt-3]

E2 retained per-source const declarations but left the const READ path on the
global last-wins `module_const_map`, so a module's OWN reference to a same-name
const bound the LAST global author (F2: a.sx `K::1`, b.sx `K::2`, main flat-imports
both → both read B's K). Complete the const analog of the type (`selectNominalLeaf`)
and callable (`selectPlainCallableAuthor`) source-aware models.

- `selectModuleConst`: own-wins; exactly one flat-visible author → it; ≥2 distinct
  flat-visible → `.ambiguous` (loud diagnostic, consistent with 0755/0724); none
  → `.none`. Reads the SELECTED author's per-source value (`module_consts_by_source`)
  and folds its RHS over the global leaf map, so a const-EXPRESSION chain
  (`N :: M + 1`, M flat-imported) still resolves M.
- Rewire `comptimeIntNamed` / `lookupFloatName` / `nameIsFloatTyped`, the runtime
  identifier path, and the global-init-from-const path through it; drop the now
  subsumed `moduleConstBareInvisible` gate.
- program_index: `moduleConst{Int,Float,IsFloatTyped}With` fold a selected `ci`.

Examples: 0759 (own-wins value const, a=1 b=2) + 0760 (two-flat-visible →
ambiguous). Single-author byte-identical (run_examples 498/0, 496 prior unchanged;
zig build test 423/423; corpus sweep 515 no-crash; m3te ios-sim exit 0).
This commit is contained in:
agra
2026-06-08 00:32:07 +03:00
parent 66d10c00bb
commit 72f06a109b
14 changed files with 181 additions and 38 deletions

View File

@@ -1379,8 +1379,18 @@ pub const Lowering = struct {
// A global initialized from a module constant copies the
// constant's recorded value (typed module consts land in
// `module_const_map` via `registerTypedModuleConst`, run in the
// same pass-2 before this).
if (self.program_index.module_const_map.get(id.name)) |ci| {
// 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,
.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;
}
if (self.diagnostics) |d|
@@ -3895,13 +3905,29 @@ pub const Lowering = struct {
break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty);
}
// Check module-level value constants (e.g. AF_INET :s32: 2)
if (self.program_index.module_const_map.get(id.name)) |ci| {
if (self.program_index.module_const_map.get(id.name)) |ci_global| {
if (!self.isNameVisible(id.name)) {
if (self.diagnostics) |d|
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name});
break :blk self.emitError(id.name, node.span);
}
break :blk self.emitModuleConst(ci);
// E2/F2: emit the SOURCE-AWARE author's value (own-wins), not
// the global last-wins `ci_global`. ≥2 flat-visible same-name
// const authors → a loud ambiguity (issue 0105 / 0760), never a
// 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),
.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});
// One diagnostic only (mirrors the type ambiguity at
// `resolveNominalLeaf`): a bare placeholder, no second
// "unresolved" cascade from `emitError`.
break :blk self.emitPlaceholder(id.name);
},
.none => break :blk self.emitModuleConst(ci_global),
}
}
// Check if it's a function name — produce function pointer reference
// Resolve mangled name for block-local functions
@@ -13385,8 +13411,10 @@ pub const Lowering = struct {
/// `evalConstIntExpr` delegation inside `evalConstFloatExpr`; this surfaces the
/// non-integral float const so the rule can reject it.
pub fn lookupFloatName(self: *Lowering, name: []const u8) ?f64 {
if (self.moduleConstBareInvisible(name)) return null;
return program_index_mod.moduleConstFloat(&self.program_index.module_const_map, &self.module.types, name);
return switch (self.selectModuleConst(name)) {
.resolved => |ci| program_index_mod.moduleConstFloatWith(&self.program_index.module_const_map, &self.module.types, name, ci),
.ambiguous, .none => null,
};
}
/// True iff `name` is a FLOAT-valued module const (`F : f64 : 2.5`,
@@ -13396,8 +13424,10 @@ pub const Lowering = struct {
/// value bindings are always integer-valued, so only the module-const table
/// can name a float.
pub fn nameIsFloatTyped(self: *Lowering, name: []const u8) bool {
if (self.moduleConstBareInvisible(name)) return false;
return program_index_mod.moduleConstIsFloatTyped(&self.program_index.module_const_map, &self.module.types, name);
return switch (self.selectModuleConst(name)) {
.resolved => |ci| program_index_mod.moduleConstIsFloatTypedWith(&self.program_index.module_const_map, &self.module.types, name, ci),
.ambiguous, .none => false,
};
}
/// Resolve a name to a compile-time integer across the three const tables.
@@ -13409,43 +13439,66 @@ pub const Lowering = struct {
if (self.comptime_value_bindings) |cvb| {
if (cvb.get(name)) |v| return v;
}
// Folded req #1: gate the bare module const on source-aware visibility
// before reading the global map (see `moduleConstBareInvisible`).
if (self.moduleConstBareInvisible(name)) return null;
// The module-const branch is shared verbatim with the stateless
// registration-time resolver (`type_bridge`) so a `[N]T` dimension
// resolves to the same length on both paths (issue 0083).
return program_index_mod.moduleConstInt(&self.program_index.module_const_map, &self.module.types, name);
// E2/F2: select the SOURCE-AWARE module-const author (own-wins; ≥2
// flat-visible → ambiguous → no value here, the loud diagnostic is the
// reference site's job). The selected `ci`'s RHS is folded over the global
// leaf map, so a const-EXPRESSION chain (`N :: M + 1`, M flat-imported)
// still resolves `M` exactly as before. Single-author selects the same
// `ci` the global map holds, so this is byte-identical to the legacy read.
return switch (self.selectModuleConst(name)) {
.resolved => |ci| program_index_mod.moduleConstIntWith(&self.program_index.module_const_map, &self.module.types, name, ci),
.ambiguous, .none => null,
};
}
/// Folded req #1: TRUE iff `name` is a module const that is NOT reachable
/// bare from the querying module — the source-aware gate every Lowering-side
/// comptime `module_const_map` reader (`comptimeIntNamed` / `lookupFloatName`
/// / `nameIsFloatTyped`) consults before the global first-match. A
/// namespaced-only import's const must be qualified (`ns.X`); without this
/// gate a bare reference leaks into a comptime-scalar / array-dim position
/// through the global table (the int folder even falls back to the float
/// reader, so all three must gate). The value itself is still folded over the
/// global map, so a cross-module const CHAIN (`N :: M + 1`, M flat-imported)
/// resolves exactly as before; the stateless `type_bridge` registration path
/// keeps the global reader this step. A main-file body carries a null
/// `current_source_file` (it IS the root), so the querying module is
/// `main_file` there; a fully unwired index (no source at all) falls open.
fn moduleConstBareInvisible(self: *Lowering, name: []const u8) bool {
const from = self.current_source_file orelse self.main_file orelse return false;
/// 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
/// collector and reads the value from the SELECTED author's per-source cache
/// (`module_consts_by_source`), never the global last-wins `module_const_map`:
///
/// - **own-wins**: the querying module's OWN const author is selected outright.
/// - else the FLAT-import-reachable const authors: exactly one → it; ≥2 distinct
/// → `.ambiguous` (issue 0105 / 0760 — never a silent first-/last-wins pick).
/// - none visible → `.none` (a namespaced-only const must be qualified `ns.X`;
/// a non-const name folds to `.none` too).
///
/// A main-file body carries a null `current_source_file` (it IS the root), so
/// the querying module is `main_file` there; a fully unwired index (no source
/// 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,
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 };
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.sourceHasModuleConst(o.source, name)) return false;
for (set.flat) |fa| if (self.sourceHasModuleConst(fa.source, name)) return false;
return true;
if (set.own) |o| if (self.sourceModuleConst(o.source, name)) |ci| return .{ .resolved = ci };
var the_one: ?program_index_mod.ModuleConstInfo = 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;
}
if (the_one) |ci| return .{ .resolved = ci };
return .none;
}
/// True iff `source`'s per-source const cache declares `name` (E0's
/// `module_consts_by_source` write side).
fn sourceHasModuleConst(self: *Lowering, source: []const u8, name: []const u8) bool {
const inner = self.program_index.module_consts_by_source.get(source) orelse return false;
return inner.contains(name);
/// `source`'s per-source const cache entry for `name` (E0's
/// `module_consts_by_source` write side), or null.
fn sourceModuleConst(self: *Lowering, source: []const u8, name: []const u8) ?program_index_mod.ModuleConstInfo {
const inner = self.program_index.module_consts_by_source.get(source) orelse return null;
return inner.get(name);
}
/// Resolve a type node, checking type_bindings first for generic type params.

View File

@@ -134,6 +134,11 @@ fn isFloatConstType(ty: TypeId) bool {
fn moduleConstFloatValuedFramed(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, parent: ?*const ModuleConstFrame) bool {
if (moduleConstFrameContains(parent, name)) return false;
const ci = consts.get(name) orelse return false;
return foldConstFloatValued(consts, table, name, ci, parent);
}
/// Get-less core of `moduleConstFloatValuedFramed`.
fn foldConstFloatValued(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo, parent: ?*const ModuleConstFrame) bool {
if (isFloatConstType(ci.ty)) return true;
var frame = ModuleConstFrame{ .name = name, .parent = parent };
return isFloatValuedExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame });
@@ -160,11 +165,27 @@ fn isCountableConstType(table: *const types.TypeTable, ty: TypeId) bool {
fn moduleConstIntFramed(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, parent: ?*const ModuleConstFrame) ?i64 {
if (moduleConstFrameContains(parent, name)) return null;
const ci = consts.get(name) orelse return null;
return foldConstInt(consts, table, name, ci, parent);
}
/// Fold a SELECTED `ModuleConstInfo`'s RHS to an integer count, the get-less core
/// of `moduleConstIntFramed`. `name` keys the cycle frame; `consts` is the LEAF
/// map a const-EXPRESSION RHS (`N :: M + 1`) resolves `M` through.
fn foldConstInt(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo, parent: ?*const ModuleConstFrame) ?i64 {
if (!isCountableConstType(table, ci.ty)) return null;
var frame = ModuleConstFrame{ .name = name, .parent = parent };
return evalConstIntExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame });
}
/// `moduleConstInt` for a const whose AUTHOR was already selected source-aware
/// (E2): fold the provided `ci`'s RHS instead of `consts.get(name)`, so a
/// same-name shadow resolves to ITS OWN value while a const-EXPRESSION leaf still
/// resolves through the global `consts` map (the chain reader). Single-author →
/// byte-identical to `moduleConstInt` (the selected `ci` IS the global one).
pub fn moduleConstIntWith(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo) ?i64 {
return foldConstInt(consts, table, name, ci, null);
}
/// A name bound to a module-global integer constant → its value, else null.
/// SINGLE source for both array-dimension resolvers — the stateful
/// body-lowering path (`Lowering.comptimeIntNamed`) and the stateless
@@ -195,6 +216,11 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), table:
fn moduleConstFloatFramed(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, parent: ?*const ModuleConstFrame) ?f64 {
if (moduleConstFrameContains(parent, name)) return null;
const ci = consts.get(name) orelse return null;
return foldConstFloat(consts, table, name, ci, parent);
}
/// Float counterpart of `foldConstInt` — the get-less core of `moduleConstFloatFramed`.
fn foldConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo, parent: ?*const ModuleConstFrame) ?f64 {
if (!isCountableConstType(table, ci.ty)) return null;
var frame = ModuleConstFrame{ .name = name, .parent = parent };
return evalConstFloatExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame });
@@ -204,6 +230,12 @@ pub fn moduleConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table
return moduleConstFloatFramed(consts, table, name, null);
}
/// `moduleConstFloat` for a source-aware-selected `ci` (E2) — the float analog
/// of `moduleConstIntWith`.
pub fn moduleConstFloatWith(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo) ?f64 {
return foldConstFloat(consts, table, name, ci, null);
}
/// True iff `name` is a FLOAT-valued module const — judged by VALUE, so it covers
/// a typed float const (`K : f64 : 4.0`), an untyped float-EXPRESSION const
/// (`ME :: 4.0 + 1.0`, whose placeholder type is `s64`), and a non-integral float
@@ -214,6 +246,11 @@ pub fn moduleConstIsFloatTyped(consts: *const std.StringHashMap(ModuleConstInfo)
return moduleConstFloatValuedFramed(consts, table, name, null);
}
/// `moduleConstIsFloatTyped` for a source-aware-selected `ci` (E2).
pub fn moduleConstIsFloatTypedWith(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo) bool {
return foldConstFloatValued(consts, table, name, ci, null);
}
/// True iff `node` is a FLOAT-valued compile-time expression — a float literal,
/// a float-typed const leaf (`F : f64 : 2.5`, `K : f64 : 4.0`), a builtin float
/// numeric-limit (`f64.max`), or arithmetic over any of those. THE predicate the