diff --git a/examples/0786-modules-same-name-const-own.sx b/examples/0786-modules-same-name-const-own.sx new file mode 100644 index 0000000..fc71d2d --- /dev/null +++ b/examples/0786-modules-same-name-const-own.sx @@ -0,0 +1,14 @@ +// 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: `a_k` returns 1 and `b_k` returns 2, +// resolved by the source-aware const author selector (`selectModuleConst`). +#import "modules/std.sx"; +#import "0786-modules-same-name-const-own/a.sx"; +#import "0786-modules-same-name-const-own/b.sx"; + +main :: () -> s32 { + print("a={} b={}\n", a_k(), b_k()); + 0 +} diff --git a/examples/0786-modules-same-name-const-own/a.sx b/examples/0786-modules-same-name-const-own/a.sx new file mode 100644 index 0000000..66c7c26 --- /dev/null +++ b/examples/0786-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/0786-modules-same-name-const-own/b.sx b/examples/0786-modules-same-name-const-own/b.sx new file mode 100644 index 0000000..16ae78a --- /dev/null +++ b/examples/0786-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`. Each `K` is selected per declaring source, so B's `b_k` reads B's value +// while A's `a_k` reads A's, never the global last-wins const. +K :: 2; +b_k :: () -> s64 { return K; } diff --git a/examples/0787-modules-same-name-const-ambiguous.sx b/examples/0787-modules-same-name-const-ambiguous.sx new file mode 100644 index 0000000..438e2ad --- /dev/null +++ b/examples/0787-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 "0787-modules-same-name-const-ambiguous/a.sx"; +#import "0787-modules-same-name-const-ambiguous/b.sx"; + +main :: () -> s32 { + print("K={}\n", K); + 0 +} diff --git a/examples/0787-modules-same-name-const-ambiguous/a.sx b/examples/0787-modules-same-name-const-ambiguous/a.sx new file mode 100644 index 0000000..cf57dc8 --- /dev/null +++ b/examples/0787-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/0787-modules-same-name-const-ambiguous/b.sx b/examples/0787-modules-same-name-const-ambiguous/b.sx new file mode 100644 index 0000000..386c1a7 --- /dev/null +++ b/examples/0787-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/0788-modules-same-name-const-expr-chain-dim.sx b/examples/0788-modules-same-name-const-expr-chain-dim.sx new file mode 100644 index 0000000..1ce3b97 --- /dev/null +++ b/examples/0788-modules-same-name-const-expr-chain-dim.sx @@ -0,0 +1,18 @@ +// issue 0105 / F2 / R1 — same-name const EXPRESSION CHAIN, coherent across a +// value read AND an array dimension. Two flat-imported modules each declare a +// same-name `M` and a same-name `K :: M + 1` that reads `M`. Each module uses +// ITS OWN `K` both as a runtime value (`return K`) and as an array dimension +// (`[K]u8`). +// +// The fold of a SELECTED const's RHS must resolve nested same-name leaves (the +// `M` inside `K :: M + 1`) in the SELECTED author's source context, not through +// the global last-wins `module_const_map`. Both observables agree per module: +// a_len=2 a_val=2, b_len=11 b_val=11. +#import "modules/std.sx"; +#import "0788-modules-same-name-const-expr-chain-dim/a.sx"; +#import "0788-modules-same-name-const-expr-chain-dim/b.sx"; + +main :: () -> s32 { + print("a_len={} a_val={} b_len={} b_val={}\n", a_len(), a_val(), b_len(), b_val()); + 0 +} diff --git a/examples/0788-modules-same-name-const-expr-chain-dim/a.sx b/examples/0788-modules-same-name-const-expr-chain-dim/a.sx new file mode 100644 index 0000000..a806828 --- /dev/null +++ b/examples/0788-modules-same-name-const-expr-chain-dim/a.sx @@ -0,0 +1,9 @@ +// Module A authors its OWN chain: `M :: 1`, `K :: M + 1` (= 2). Both the value +// read and the array dimension must resolve `K` through A's `M`. +M :: 1; +K :: M + 1; +a_val :: () -> s64 { return K; } +a_len :: () -> s64 { + arr : [K]u8 = ---; + return arr.len; +} diff --git a/examples/0788-modules-same-name-const-expr-chain-dim/b.sx b/examples/0788-modules-same-name-const-expr-chain-dim/b.sx new file mode 100644 index 0000000..a6a45c9 --- /dev/null +++ b/examples/0788-modules-same-name-const-expr-chain-dim/b.sx @@ -0,0 +1,10 @@ +// Module B authors a DIFFERENT same-name chain: `M :: 10`, `K :: M + 1` (= 11). +// A shadow of A's `M`/`K`. Each `K`'s RHS leaf resolves to its own source's `M`, +// so the dimension fold gives B's length 11 — never A's via the global map. +M :: 10; +K :: M + 1; +b_val :: () -> s64 { return K; } +b_len :: () -> s64 { + arr : [K]u8 = ---; + return arr.len; +} diff --git a/examples/0789-modules-same-name-const-leaf-author-pin.sx b/examples/0789-modules-same-name-const-leaf-author-pin.sx new file mode 100644 index 0000000..0e5eddf --- /dev/null +++ b/examples/0789-modules-same-name-const-leaf-author-pin.sx @@ -0,0 +1,22 @@ +// issue 0105 / F1 — a UNIQUE expression const's nested leaf folds against the +// const's AUTHOR source, not the reading module's. `a.sx` declares `M :: 1` and +// `K :: M + 1` (= 2); `b.sx` declares a DIFFERENT same-name `M :: 10` (no `K`). +// `main` flat-imports both, so the reader sees two `M`s — but it reads only `K`, +// which is unique to `a.sx`. Folding `K`'s RHS must pin `M` to A's source (→ 1), +// giving `K = 2`, coherently whether `K` is read as a runtime VALUE (`print K`) +// or used as an array DIMENSION (`[K]u8`) → val=2 len=2. Without the author pin +// the nested leaf would re-select `M` from `main`'s view (two `M`s → ambiguous / +// non-const dimension). +#import "modules/std.sx"; +#import "0789-modules-same-name-const-leaf-author-pin/a.sx"; +#import "0789-modules-same-name-const-leaf-author-pin/b.sx"; + +read_dim :: () -> s64 { + arr : [K]u8 = ---; + return arr.len; +} + +main :: () -> s32 { + print("val={} len={}\n", K, read_dim()); + 0 +} diff --git a/examples/0789-modules-same-name-const-leaf-author-pin/a.sx b/examples/0789-modules-same-name-const-leaf-author-pin/a.sx new file mode 100644 index 0000000..dd7c9dd --- /dev/null +++ b/examples/0789-modules-same-name-const-leaf-author-pin/a.sx @@ -0,0 +1,5 @@ +// Module A authors `M :: 1` and the EXPRESSION const `K :: M + 1` (= 2). `K` is +// unique across the program; only A defines it. Its RHS leaf `M` must always +// fold against A's `M` (= 1), no matter which module reads `K`. +M :: 1; +K :: M + 1; diff --git a/examples/0789-modules-same-name-const-leaf-author-pin/b.sx b/examples/0789-modules-same-name-const-leaf-author-pin/b.sx new file mode 100644 index 0000000..b2fe411 --- /dev/null +++ b/examples/0789-modules-same-name-const-leaf-author-pin/b.sx @@ -0,0 +1,5 @@ +// Module B authors only a DIFFERENT same-name `M :: 10` — a shadow of A's `M`, +// with NO `K`. When `main` flat-imports both A and B, the reading module sees +// two `M`s; folding A's `K :: M + 1` must NOT use this `M` (which would make `M` +// ambiguous from the reader's view) — it must pin to A's `M`. +M :: 10; diff --git a/examples/0790-modules-same-name-const-cross-cycle-guard.sx b/examples/0790-modules-same-name-const-cross-cycle-guard.sx new file mode 100644 index 0000000..28d42e9 --- /dev/null +++ b/examples/0790-modules-same-name-const-cross-cycle-guard.sx @@ -0,0 +1,16 @@ +// issue 0105 / F3 — the const cycle guard must key on (name, AUTHOR-source), not +// name alone. `a.sx` declares `M :: 1` and `K :: M + 1` (= 2). `b.sx` flat-imports +// `a.sx` and declares a DIFFERENT same-name `M :: K + 1` (= 3) — so the SAME name +// `M` appears at two levels of one fold chain, in two different modules +// (b's `M` → a's `K` → a's `M`). A name-only cycle guard sees `M` twice and trips +// a FALSE cycle, folding b's `M` to null → the array dimension `[M]u8` becomes a +// non-const error. Keyed on (name, source) the two `M`s are distinct, so the +// chain folds: `m=3 len=3`, coherent for the value read and the dimension. +#import "modules/std.sx"; +#import "0790-modules-same-name-const-cross-cycle-guard/b.sx"; + +main :: () -> s32 { + arr : [M]u8 = ---; + print("m={} len={}\n", M, arr.len); + 0 +} diff --git a/examples/0790-modules-same-name-const-cross-cycle-guard/a.sx b/examples/0790-modules-same-name-const-cross-cycle-guard/a.sx new file mode 100644 index 0000000..e21aab0 --- /dev/null +++ b/examples/0790-modules-same-name-const-cross-cycle-guard/a.sx @@ -0,0 +1,4 @@ +// Module A authors `M :: 1` and `K :: M + 1` (= 2). A's `K` folds its leaf `M` +// against A's own `M`. +M :: 1; +K :: M + 1; diff --git a/examples/0790-modules-same-name-const-cross-cycle-guard/b.sx b/examples/0790-modules-same-name-const-cross-cycle-guard/b.sx new file mode 100644 index 0000000..d0e19f0 --- /dev/null +++ b/examples/0790-modules-same-name-const-cross-cycle-guard/b.sx @@ -0,0 +1,6 @@ +// Module B flat-imports A and authors a DIFFERENT same-name `M :: K + 1` (= 3), +// reading A's `K`. Folding B's `M` walks B's `M` → A's `K` → A's `M` — the name +// `M` recurs across two modules. The cycle guard must NOT confuse B's `M` with +// A's `M`: keyed on (name, source) the chain folds to 3. +#import "a.sx"; +M :: K + 1; diff --git a/examples/0791-modules-same-name-const-multi-level-cross-module.sx b/examples/0791-modules-same-name-const-multi-level-cross-module.sx new file mode 100644 index 0000000..7116c0b --- /dev/null +++ b/examples/0791-modules-same-name-const-multi-level-cross-module.sx @@ -0,0 +1,19 @@ +// issue 0105 / F1 / R1 — a MULTI-LEVEL cross-module const chain pins EACH fold +// level to its own author. `a.sx` declares `M :: 1`, `K :: M + 1` (= 2). `b.sx` +// declares a full same-name shadow `M :: 10`, `K :: M + 1` (= 11). `c.sx` +// flat-imports `a.sx` only and declares `BIG :: K + 100` — its `K` is A's, so +// `BIG` = 102. `main` flat-imports `b.sx` and `c.sx`. +// +// Reading `BIG` walks BIG (c) → K (a) → M (a): each level resolves in its OWN +// author's context, so `BIG` folds A's chain (= 102) even though `main` itself +// sees only B's `K` (= 11) bare. If any level leaked to the reader's view, `BIG` +// would fold B's `K` (→ 111). `#import` is non-transitive, so `K` bare in `main` +// is B's (A is reachable only through C) → bk=11. Output: big=102 bk=11. +#import "modules/std.sx"; +#import "0791-modules-same-name-const-multi-level-cross-module/b.sx"; +#import "0791-modules-same-name-const-multi-level-cross-module/c.sx"; + +main :: () -> s32 { + print("big={} bk={}\n", BIG, K); + 0 +} diff --git a/examples/0791-modules-same-name-const-multi-level-cross-module/a.sx b/examples/0791-modules-same-name-const-multi-level-cross-module/a.sx new file mode 100644 index 0000000..1b1e9a5 --- /dev/null +++ b/examples/0791-modules-same-name-const-multi-level-cross-module/a.sx @@ -0,0 +1,4 @@ +// Module A authors the base chain `M :: 1`, `K :: M + 1` (= 2). Reached only via +// C's flat import — never bare-visible in `main`. +M :: 1; +K :: M + 1; diff --git a/examples/0791-modules-same-name-const-multi-level-cross-module/b.sx b/examples/0791-modules-same-name-const-multi-level-cross-module/b.sx new file mode 100644 index 0000000..2c37eba --- /dev/null +++ b/examples/0791-modules-same-name-const-multi-level-cross-module/b.sx @@ -0,0 +1,4 @@ +// Module B authors a full same-name shadow `M :: 10`, `K :: M + 1` (= 11). `main` +// flat-imports B, so bare `K` in `main` is B's (= 11). +M :: 10; +K :: M + 1; diff --git a/examples/0791-modules-same-name-const-multi-level-cross-module/c.sx b/examples/0791-modules-same-name-const-multi-level-cross-module/c.sx new file mode 100644 index 0000000..6a074be --- /dev/null +++ b/examples/0791-modules-same-name-const-multi-level-cross-module/c.sx @@ -0,0 +1,5 @@ +// Module C flat-imports A only and authors `BIG :: K + 100`. Its bare `K` is A's +// (= 2), so `BIG` folds to 102. The leaf must pin to A across the import into +// `main`, which itself sees a different same-name `K` (B's = 11). +#import "a.sx"; +BIG :: K + 100; diff --git a/examples/0792-modules-same-name-const-struct-field-dim.sx b/examples/0792-modules-same-name-const-struct-field-dim.sx new file mode 100644 index 0000000..ac314cd --- /dev/null +++ b/examples/0792-modules-same-name-const-struct-field-dim.sx @@ -0,0 +1,15 @@ +// issue 0105 / F2 (#6 registration-time reader) — a same-name VALUE const used as +// a STRUCT FIELD array dimension, baked into the layout at REGISTRATION time, is +// source-aware. Two flat-imported modules each declare a same-name `K` and a +// same-name struct `Box { arr: [K]u8 }`. `size_of(Box)` in each module must use +// ITS OWN `K` for the field dimension (own-wins), so the layouts differ: +// a_sz=2, b_sz=7. The registration-time field-type resolution routes through the +// stateful source-aware const reader, not the global last-wins map. +#import "modules/std.sx"; +#import "0792-modules-same-name-const-struct-field-dim/a.sx"; +#import "0792-modules-same-name-const-struct-field-dim/b.sx"; + +main :: () -> s32 { + print("a_sz={} b_sz={}\n", a_sz(), b_sz()); + 0 +} diff --git a/examples/0792-modules-same-name-const-struct-field-dim/a.sx b/examples/0792-modules-same-name-const-struct-field-dim/a.sx new file mode 100644 index 0000000..d25b22a --- /dev/null +++ b/examples/0792-modules-same-name-const-struct-field-dim/a.sx @@ -0,0 +1,5 @@ +// Module A authors `K :: 2` and a struct `Box` whose array field is dimensioned +// by A's `K`. `size_of(Box)` folds the field dimension against A's `K` (= 2). +K :: 2; +Box :: struct { arr: [K]u8; } +a_sz :: () -> s64 { return size_of(Box); } diff --git a/examples/0792-modules-same-name-const-struct-field-dim/b.sx b/examples/0792-modules-same-name-const-struct-field-dim/b.sx new file mode 100644 index 0000000..2435256 --- /dev/null +++ b/examples/0792-modules-same-name-const-struct-field-dim/b.sx @@ -0,0 +1,6 @@ +// Module B authors a DIFFERENT same-name `K :: 7` and same-name struct `Box`. +// `size_of(Box)` folds the field dimension against B's `K` (= 7), so the layout +// differs from A's — never collapsed to a global last-wins `K`. +K :: 7; +Box :: struct { arr: [K]u8; } +b_sz :: () -> s64 { return size_of(Box); } diff --git a/src/imports.zig b/src/imports.zig index af535cd..71d334a 100644 --- a/src/imports.zig +++ b/src/imports.zig @@ -314,8 +314,8 @@ pub const ResolvedModule = struct { try self.scope.put(name, {}); if (seen_list.contains(name)) { // A cross-module name collision: drop from the global list - // (first-wins) UNLESS this is a per-source decl (a named type or - // a type-alias const), which must reach registration as a + // (first-wins) UNLESS this is a per-source decl (a type, alias, + // or non-function const), which must reach registration as a // distinct author of its own module (issues 0104/0105). append_to_global = isPerSourceDecl(decl); } else { @@ -352,14 +352,14 @@ pub const ResolvedModule = struct { if (decl.data.declName()) |name| { if (seen_list.contains(name)) { // First-wins on a cross-module name collision — EXCEPT a - // per-source decl (a named type or a type-alias const), each - // of which must reach registration as a distinct same-name - // author of its own module (issues 0104/0105). FUNCTIONS and - // VALUE consts keep first-wins (issue 0102 — the shadowed - // function stays reachable via its qualified name / - // SelectedFunc; same-name value consts are deferred to step - // E5). Node identity (above) still de-dups a diamond import of - // the SAME decl. + // per-source decl (a named type, or any non-function const: + // type alias + value const), each of which must reach + // registration as a distinct same-name author of its own + // module (issues 0104/0105 types, step E5 value consts). Only + // FUNCTIONS keep first-wins (issue 0102 — the shadowed author + // stays reachable via its qualified name / SelectedFunc). + // Node identity (above) still de-dups a diamond import of the + // SAME decl. if (!isPerSourceDecl(decl)) continue; } else { try seen_list.put(name, {}); @@ -372,43 +372,19 @@ pub const ResolvedModule = struct { /// A decl that must register PER-SOURCE: each same-name author across modules /// registers against its OWN module rather than collapsing to a single - /// first-wins winner. NAMED types and TYPE-introducing `const_decl`s (type - /// aliases + inline type decls, source-keyed via the alias cache) are - /// per-source — that is what closes issues 0104/0105 for types and aliases. - /// Everything else keeps the first-wins name-merge: - /// - FUNCTIONS (issue 0102 — the shadowed author stays reachable via its - /// qualified name / SelectedFunc), - /// - VALUE `const_decl`s (literal / value-expression RHS): a same-name - /// value const keeps the pre-E2 first-wins read; cross-module same-name - /// value-const support is a separate concern (step E5), NOT part of the - /// 0105 type close, - /// - and `var_decl`s, including a `#foreign` extern global declared in two - /// files (e.g. `__stdinp : *void #foreign;`) that MUST resolve to the ONE - /// libSystem symbol, not split into a duplicate `__stdinp.1`. + /// first-wins winner. NAMED types and every non-function `const_decl` (type + /// aliases + inline type decls + VALUE consts, source-keyed via the alias / + /// const caches) are per-source — that is what closes issues 0104/0105 for + /// types/aliases and supports same-name value consts (step E5). Everything + /// else keeps the first-wins name-merge: FUNCTIONS (issue 0102 — the shadowed + /// author stays reachable via its qualified name / SelectedFunc), and crucially + /// `var_decl`s, including a `#foreign` extern global declared in two files + /// (e.g. `__stdinp : *void #foreign;`) that MUST resolve to the ONE libSystem + /// symbol, not split into a duplicate `__stdinp.1`. fn isPerSourceDecl(decl: *const Node) bool { return switch (decl.data) { .struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => true, - // A `const_decl` is per-source ONLY when its RHS introduces a TYPE - // (alias / inline type decl). A VALUE const — literal or value - // expression — and a function const keep the first-wins merge. - .const_decl => |cd| switch (cd.value.data) { - .fn_decl, - .int_literal, - .float_literal, - .bool_literal, - .string_literal, - .null_literal, - .undef_literal, - .enum_literal, - .struct_literal, - .array_literal, - .tuple_literal, - .binary_op, - .unary_op, - .chained_comparison, - => false, - else => true, - }, + .const_decl => |cd| cd.value.data != .fn_decl, else => false, }; } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index e6a303a..506ac87 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -54,6 +54,60 @@ fn isExportedEntryName(name: []const u8) bool { std.mem.startsWith(u8, name, "Java_"); } +/// One frame in the chain of module-const names currently being folded by the +/// SOURCE-AWARE const evaluator (`Lowering.foldSourceConstInt` and its float +/// twins). Stack-allocated per recursive frame, so cycle detection needs no +/// allocation — the source-aware analogue of `program_index.ModuleConstFrame`, +/// which guards the GLOBAL-map fold (`moduleConstInt`). The frame keys on the +/// const's (name, author-source) pair, NOT name alone: same-name nested consts +/// across modules (`a.M` ≠ `b.M`) must NOT trip a false cycle (F3). A pair +/// already on the chain is a cyclic definition (`N :: N`; `N :: M + 1; M :: N`) +/// with no compile-time value → folds to null. +const ConstFoldFrame = struct { + name: []const u8, + source: ?[]const u8, + parent: ?*const ConstFoldFrame, +}; + +fn constFoldFrameContains(frame: ?*const ConstFoldFrame, name: []const u8, source: ?[]const u8) bool { + var cur = frame; + while (cur) |c| : (cur = c.parent) { + if (std.mem.eql(u8, c.name, name) and sourcesEql(c.source, source)) return true; + } + return false; +} + +fn sourcesEql(a: ?[]const u8, b: ?[]const u8) bool { + if (a == null and b == null) return true; + if (a == null or b == null) return false; + return std.mem.eql(u8, a.?, b.?); +} + +/// Folding context for a SOURCE-AWARE module-const EXPRESSION RHS (E2/F2/R1). +/// The leaf-resolution twin of `program_index.ModuleConstCtx`, but every leaf +/// name resolves through the querying source's OWN const author +/// (`selectModuleConst`, own-wins / ambiguous) instead of the GLOBAL last-wins +/// `module_const_map`. This is what makes a same-name shadow's RHS chain +/// (`K :: M + 1`, with `M` a same-name shadow too) fold `M` to the SELECTED +/// author's `M` — coherently for a const used as a value AND as an array +/// dimension / count. `frame` is the cyclic-definition guard. +const SourceConstCtx = struct { + lowering: *Lowering, + frame: ?*const ConstFoldFrame, + pub fn lookupDimName(self: SourceConstCtx, name: []const u8) ?i64 { + return self.lowering.foldSourceConstInt(name, self.frame); + } + pub fn lookupPackLen(self: SourceConstCtx, name: []const u8) ?i64 { + return self.lowering.lookupPackLen(name); + } + pub fn lookupFloatName(self: SourceConstCtx, name: []const u8) ?f64 { + return self.lowering.foldSourceConstFloat(name, self.frame); + } + pub fn nameIsFloatTyped(self: SourceConstCtx, name: []const u8) bool { + return self.lowering.sourceConstIsFloatTyped(name, self.frame); + } +}; + // ── Scope ─────────────────────────────────────────────────────────────── pub const Binding = struct { @@ -1387,9 +1441,22 @@ 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| { - if (self.constExprValue(ci.value, var_ty)) |cv| break :blk cv; + // same pass-2 before this). F1/F2: copy the SOURCE-AWARE author's + // value (own-wins), folding its RHS in the author's context, and + // reject a ≥2-flat ambiguity loudly. + if (self.program_index.module_const_map.get(id.name)) |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; + }, + }; + 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 }); @@ -4032,13 +4099,26 @@ 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); + // 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 => |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}); + break :blk self.emitPlaceholder(id.name); + }, + .none => break :blk self.emitModuleConst(ci_global, null), + } } // Check if it's a function name — produce function pointer reference // Resolve mangled name for block-local functions @@ -13640,8 +13720,7 @@ 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 self.foldSourceConstFloat(name, null); } /// True iff `name` is a FLOAT-valued module const (`F : f64 : 2.5`, @@ -13651,11 +13730,13 @@ 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 self.sourceConstIsFloatTyped(name, null); } /// Resolve a name to a compile-time integer across the three const tables. + /// A comptime binding (generic value param / inline-for cursor) or a + /// `#run`/`OS`/`ARCH` comptime constant wins first; otherwise the name is a + /// SOURCE-AWARE module const, folded with nested leaves resolved own-wins. fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 { if (self.comptime_constants.get(name)) |cv| switch (cv) { .int_val => |iv| return iv, @@ -13664,43 +13745,149 @@ 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); + return self.foldSourceConstInt(name, 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; + /// Source-aware INTEGER fold of a module const `name` (E2/F2/R1). Select the + /// SOURCE-AWARE author (own-wins; ≥2 flat-visible → ambiguous → null, the loud + /// diagnostic is the reference site's job), then fold ITS RHS with nested const + /// leaves resolved through `SourceConstCtx` — each leaf re-selects its OWN + /// source author, NOT the global last-wins `module_const_map`. So a shadowed + /// `K :: M + 1` folds `M` to the SELECTED author's `M`, coherently whether `K` + /// is read as a value (`return K`) or used as an array dimension / count + /// (`[K]u8`). `frame` (keyed by name + author-source, F3) cycle-guards a const + /// whose value references another const. Single-author → byte-identical to the + /// legacy fold (the selected `ci` IS the global one and every nested leaf has + /// exactly one author). + fn foldSourceConstInt(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?i64 { + return switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (constFoldFrameContains(frame, name, sel.source)) return null; + if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + 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, + }; + } + + /// Float counterpart of `foldSourceConstInt` (E2/F2/R1). + fn foldSourceConstFloat(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?f64 { + return switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (constFoldFrameContains(frame, name, sel.source)) return null; + if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + 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, + }; + } + + /// Source-aware "is `name` a FLOAT-valued module const" (E2/F2/R1): judge the + /// SELECTED author's value, with nested const leaves resolved source-aware. + fn sourceConstIsFloatTyped(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) bool { + return switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (constFoldFrameContains(frame, name, sel.source)) return false; + if (program_index_mod.isFloatConstType(sel.info.ty)) return true; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + 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, + }; + } + + /// 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: ModuleConstInfo, + source: ?[]const u8, + }; + + const ConstAuthor = union(enum) { + resolved: SelectedConst, + ambiguous, + none, + }; + + /// 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. + 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 = .{ .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.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 = .{ .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 = .{ .info = ci, .source = fa.source }; + } + if (the_one) |sc| return .{ .resolved = sc }; + 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) ?ModuleConstInfo { + const inner = self.program_index.module_consts_by_source.get(source) orelse return null; + 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. @@ -16821,7 +17008,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 diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index fb7e2fa..933e389 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -134,7 +134,7 @@ const ModuleConstCtx = struct { /// True iff `ty` is a float type — one half of the float-valued-const test the /// int folder's division arm relies on. Module consts only ever carry the builtin /// `f32` / `f64`. -fn isFloatConstType(ty: TypeId) bool { +pub fn isFloatConstType(ty: TypeId) bool { return ty == .f32 or ty == .f64; } @@ -163,7 +163,7 @@ fn moduleConstFloatValuedFramed(consts: *const std.StringHashMap(ModuleConstInfo /// off an integer-looking initializer (issue 0088 — the second symptom, where /// `N : string : 4` folded `[N]s64` to 4 by reading the `int_literal` node and /// ignoring the `string` annotation). -fn isCountableConstType(table: *const types.TypeTable, ty: TypeId) bool { +pub fn isCountableConstType(table: *const types.TypeTable, ty: TypeId) bool { return switch (ty) { .s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize, .f32, .f64 => true, else => if (ty.isBuiltin()) false else switch (table.get(ty)) {