diff --git a/examples/0759-modules-same-name-const-own.sx b/examples/0759-modules-same-name-const-own.sx new file mode 100644 index 0000000..2d349d9 --- /dev/null +++ b/examples/0759-modules-same-name-const-own.sx @@ -0,0 +1,16 @@ +// issue 0105 / F2 — same-name VALUE const, own-wins. Two flat-imported modules +// each declare a top-level `K` with a different value and a function that reads +// `K` bare. Each function's OWN reference must bind ITS OWN module's `K` +// (own-wins), exactly as same-name structs (0754) and functions (0722) do — +// NOT the global last-wins author. Pre-fix both `a_k` and `b_k` returned B's +// `K` (the const READ path read the global last-wins `module_const_map`); now +// `a_k` returns 1 and `b_k` returns 2, resolved by the source-aware const +// author selector (`selectModuleConst`). +#import "modules/std.sx"; +#import "0759-modules-same-name-const-own/a.sx"; +#import "0759-modules-same-name-const-own/b.sx"; + +main :: () -> s32 { + print("a={} b={}\n", a_k(), b_k()); + 0 +} diff --git a/examples/0759-modules-same-name-const-own/a.sx b/examples/0759-modules-same-name-const-own/a.sx new file mode 100644 index 0000000..66c7c26 --- /dev/null +++ b/examples/0759-modules-same-name-const-own/a.sx @@ -0,0 +1,3 @@ +// Module A authors its OWN value const `K` (1) and reads it bare. +K :: 1; +a_k :: () -> s64 { return K; } diff --git a/examples/0759-modules-same-name-const-own/b.sx b/examples/0759-modules-same-name-const-own/b.sx new file mode 100644 index 0000000..ef12778 --- /dev/null +++ b/examples/0759-modules-same-name-const-own/b.sx @@ -0,0 +1,5 @@ +// Module B authors a DIFFERENT same-name value const `K` (2) — a shadow of A's +// `K`. Pre-fix the two collapsed last-wins in the global const map, so A's `a_k` +// read B's value; now each `K` is selected per declaring source. +K :: 2; +b_k :: () -> s64 { return K; } diff --git a/examples/0760-modules-same-name-const-ambiguous.sx b/examples/0760-modules-same-name-const-ambiguous.sx new file mode 100644 index 0000000..e364abd --- /dev/null +++ b/examples/0760-modules-same-name-const-ambiguous.sx @@ -0,0 +1,14 @@ +// issue 0105 / F2 — same-name VALUE const, two-flat-visible → AMBIGUOUS. `main` +// flat-imports two modules that each author a same-name `K` and authors none +// itself. A bare `K` reference can't be disambiguated, so the compiler emits a +// LOUD diagnostic (consistent with the type ambiguity at 0755 and the function +// ambiguity at 0724) and poisons the result — never a silent first-/last-wins +// pick. +#import "modules/std.sx"; +#import "0760-modules-same-name-const-ambiguous/a.sx"; +#import "0760-modules-same-name-const-ambiguous/b.sx"; + +main :: () -> s32 { + print("K={}\n", K); + 0 +} diff --git a/examples/0760-modules-same-name-const-ambiguous/a.sx b/examples/0760-modules-same-name-const-ambiguous/a.sx new file mode 100644 index 0000000..cf57dc8 --- /dev/null +++ b/examples/0760-modules-same-name-const-ambiguous/a.sx @@ -0,0 +1,3 @@ +// One of two flat authors of value const `K`. A consumer that flat-imports BOTH +// and reads `K` bare cannot pick between them. +K :: 1; diff --git a/examples/0760-modules-same-name-const-ambiguous/b.sx b/examples/0760-modules-same-name-const-ambiguous/b.sx new file mode 100644 index 0000000..386c1a7 --- /dev/null +++ b/examples/0760-modules-same-name-const-ambiguous/b.sx @@ -0,0 +1,2 @@ +// The second flat author of value const `K`. +K :: 2; diff --git a/examples/expected/0759-modules-same-name-const-own.exit b/examples/expected/0759-modules-same-name-const-own.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0759-modules-same-name-const-own.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0759-modules-same-name-const-own.stderr b/examples/expected/0759-modules-same-name-const-own.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0759-modules-same-name-const-own.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0759-modules-same-name-const-own.stdout b/examples/expected/0759-modules-same-name-const-own.stdout new file mode 100644 index 0000000..b0ff4e7 --- /dev/null +++ b/examples/expected/0759-modules-same-name-const-own.stdout @@ -0,0 +1 @@ +a=1 b=2 diff --git a/examples/expected/0760-modules-same-name-const-ambiguous.exit b/examples/expected/0760-modules-same-name-const-ambiguous.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0760-modules-same-name-const-ambiguous.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0760-modules-same-name-const-ambiguous.stderr b/examples/expected/0760-modules-same-name-const-ambiguous.stderr new file mode 100644 index 0000000..03ed12e --- /dev/null +++ b/examples/expected/0760-modules-same-name-const-ambiguous.stderr @@ -0,0 +1,5 @@ +error: 'K' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0760-modules-same-name-const-ambiguous.sx:12:21 + | +12 | print("K={}\n", K); + | ^ diff --git a/examples/expected/0760-modules-same-name-const-ambiguous.stdout b/examples/expected/0760-modules-same-name-const-ambiguous.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0760-modules-same-name-const-ambiguous.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 9a82fc5..47ec15d 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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. diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 5b14896..3eb8957 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -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