From 5df4ac61a774c02ac00b15eb9f7ce5e43dd18822 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 21:29:31 +0300 Subject: [PATCH 1/3] fix(stdlib/E5): source-aware same-name VALUE consts (own-wins / ambiguous / cross-module expr-chains) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-land the value-const analog of the E1-E4 type work, reconciled onto the current source-keyed resolver and hardened. A same-name VALUE const declared in multiple flat-imported modules is now resolved per declaring source, not the global last-wins `module_const_map`. - imports.zig: `isPerSourceDecl` retains every non-function `const_decl` per-source (value consts + type aliases), so each same-name author reaches registration as a distinct author of its own module. Functions and var_decls keep first-wins. - lower.zig: * `selectModuleConst` over `module_consts_by_source` — own-wins; exactly one flat-visible resolves; >=2 flat-visible bare -> loud ambiguous (consistent with the 0755 type / 0724 fn / 0782 generic ambiguities). Rewires every consumer: `comptimeIntNamed`, the runtime-id read, the global-init read, and the float-name path (`lookupFloatName` / `nameIsFloatTyped`). * `SourceConstCtx` + `foldSourceConstInt`/`Float` + `sourceConstIsFloatTyped` fold a selected const's RHS with nested same-name leaves re-selected in their own author source, so VALUE and array-DIMENSION results are coherent. * `pinConstAuthorSource` pins each fold level to the SELECTED const's author (F1), including multi-level cross-module chains. * cycle guard keyed on (name, author-source), not name alone (F3), so same-name nested consts across modules do not trip a false cycle. * `emitModuleConst` takes the author source and pins while folding/lowering. Registration-time struct/inline-type field dimensions route through the now source-aware stateful reader; the type-alias dimension path resolves each alias against its own author's consts. - program_index.zig: expose `isFloatConstType` / `isCountableConstType` for the source-aware folds. examples: 0786 own-wins, 0787 ambiguous (exit 1), 0788 expr-chain value+dim coherent, 0789 leaf-author-pin, 0790 cross-module cycle-guard (F3), 0791 multi-level cross-module chain, 0792 struct-field registration-time dim. Single-author corpus byte-identical (524 prior markers green); 531 total. --- examples/0786-modules-same-name-const-own.sx | 14 + .../0786-modules-same-name-const-own/a.sx | 3 + .../0786-modules-same-name-const-own/b.sx | 5 + .../0787-modules-same-name-const-ambiguous.sx | 14 + .../a.sx | 3 + .../b.sx | 2 + ...-modules-same-name-const-expr-chain-dim.sx | 18 ++ .../a.sx | 9 + .../b.sx | 10 + ...modules-same-name-const-leaf-author-pin.sx | 22 ++ .../a.sx | 5 + .../b.sx | 5 + ...dules-same-name-const-cross-cycle-guard.sx | 16 + .../a.sx | 4 + .../b.sx | 6 + ...ame-name-const-multi-level-cross-module.sx | 19 ++ .../a.sx | 4 + .../b.sx | 4 + .../c.sx | 5 + ...odules-same-name-const-struct-field-dim.sx | 15 + .../a.sx | 5 + .../b.sx | 6 + src/imports.zig | 64 ++-- src/ir/lower.zig | 274 +++++++++++++++--- src/ir/program_index.zig | 4 +- 25 files changed, 450 insertions(+), 86 deletions(-) create mode 100644 examples/0786-modules-same-name-const-own.sx create mode 100644 examples/0786-modules-same-name-const-own/a.sx create mode 100644 examples/0786-modules-same-name-const-own/b.sx create mode 100644 examples/0787-modules-same-name-const-ambiguous.sx create mode 100644 examples/0787-modules-same-name-const-ambiguous/a.sx create mode 100644 examples/0787-modules-same-name-const-ambiguous/b.sx create mode 100644 examples/0788-modules-same-name-const-expr-chain-dim.sx create mode 100644 examples/0788-modules-same-name-const-expr-chain-dim/a.sx create mode 100644 examples/0788-modules-same-name-const-expr-chain-dim/b.sx create mode 100644 examples/0789-modules-same-name-const-leaf-author-pin.sx create mode 100644 examples/0789-modules-same-name-const-leaf-author-pin/a.sx create mode 100644 examples/0789-modules-same-name-const-leaf-author-pin/b.sx create mode 100644 examples/0790-modules-same-name-const-cross-cycle-guard.sx create mode 100644 examples/0790-modules-same-name-const-cross-cycle-guard/a.sx create mode 100644 examples/0790-modules-same-name-const-cross-cycle-guard/b.sx create mode 100644 examples/0791-modules-same-name-const-multi-level-cross-module.sx create mode 100644 examples/0791-modules-same-name-const-multi-level-cross-module/a.sx create mode 100644 examples/0791-modules-same-name-const-multi-level-cross-module/b.sx create mode 100644 examples/0791-modules-same-name-const-multi-level-cross-module/c.sx create mode 100644 examples/0792-modules-same-name-const-struct-field-dim.sx create mode 100644 examples/0792-modules-same-name-const-struct-field-dim/a.sx create mode 100644 examples/0792-modules-same-name-const-struct-field-dim/b.sx 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)) { From 189774712fddbdfc012fece7147a6987cf89c2c5 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 21:40:33 +0300 Subject: [PATCH 2/3] test(stdlib/E5): pin 0786-0792 value-const same-name golden markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track the 21 examples/expected/078x golden markers (exit/stdout/stderr for 0786-0792) generated alongside 5df4ac6. The E5 source change and example sources were committed there; these regression markers were generated on disk (the example gate passes against them) but left untracked, leaving the tree dirty and the new regressions unpinned in git. No source or golden content changes — markers verified byte-for-byte against the current binary via run_examples.sh (531 passed, 0 failed). - 0786 own-wins (a=1 b=2) - 0787 bare same-name two-flat-visible -> loud ambiguous (exit 1) - 0788 expr-chain value+dimension coherent (a_len=2 a_val=2 b_len=11 b_val=11) - 0789 imported expr-const nested leaves pinned to author source (val=2 len=2) - 0790 cross-module same-name cycle-guard, no false cycle (m=3 len=3) - 0791 multi-level cross-module chain (big=102 bk=11) - 0792 struct-field registration-time dimension (a_sz=2 b_sz=7) --- examples/expected/0786-modules-same-name-const-own.exit | 1 + examples/expected/0786-modules-same-name-const-own.stderr | 1 + examples/expected/0786-modules-same-name-const-own.stdout | 1 + .../expected/0787-modules-same-name-const-ambiguous.exit | 1 + .../expected/0787-modules-same-name-const-ambiguous.stderr | 5 +++++ .../expected/0787-modules-same-name-const-ambiguous.stdout | 1 + .../0788-modules-same-name-const-expr-chain-dim.exit | 1 + .../0788-modules-same-name-const-expr-chain-dim.stderr | 1 + .../0788-modules-same-name-const-expr-chain-dim.stdout | 1 + .../0789-modules-same-name-const-leaf-author-pin.exit | 1 + .../0789-modules-same-name-const-leaf-author-pin.stderr | 1 + .../0789-modules-same-name-const-leaf-author-pin.stdout | 1 + .../0790-modules-same-name-const-cross-cycle-guard.exit | 1 + .../0790-modules-same-name-const-cross-cycle-guard.stderr | 1 + .../0790-modules-same-name-const-cross-cycle-guard.stdout | 1 + ...791-modules-same-name-const-multi-level-cross-module.exit | 1 + ...1-modules-same-name-const-multi-level-cross-module.stderr | 1 + ...1-modules-same-name-const-multi-level-cross-module.stdout | 1 + .../0792-modules-same-name-const-struct-field-dim.exit | 1 + .../0792-modules-same-name-const-struct-field-dim.stderr | 1 + .../0792-modules-same-name-const-struct-field-dim.stdout | 1 + 21 files changed, 25 insertions(+) create mode 100644 examples/expected/0786-modules-same-name-const-own.exit create mode 100644 examples/expected/0786-modules-same-name-const-own.stderr create mode 100644 examples/expected/0786-modules-same-name-const-own.stdout create mode 100644 examples/expected/0787-modules-same-name-const-ambiguous.exit create mode 100644 examples/expected/0787-modules-same-name-const-ambiguous.stderr create mode 100644 examples/expected/0787-modules-same-name-const-ambiguous.stdout create mode 100644 examples/expected/0788-modules-same-name-const-expr-chain-dim.exit create mode 100644 examples/expected/0788-modules-same-name-const-expr-chain-dim.stderr create mode 100644 examples/expected/0788-modules-same-name-const-expr-chain-dim.stdout create mode 100644 examples/expected/0789-modules-same-name-const-leaf-author-pin.exit create mode 100644 examples/expected/0789-modules-same-name-const-leaf-author-pin.stderr create mode 100644 examples/expected/0789-modules-same-name-const-leaf-author-pin.stdout create mode 100644 examples/expected/0790-modules-same-name-const-cross-cycle-guard.exit create mode 100644 examples/expected/0790-modules-same-name-const-cross-cycle-guard.stderr create mode 100644 examples/expected/0790-modules-same-name-const-cross-cycle-guard.stdout create mode 100644 examples/expected/0791-modules-same-name-const-multi-level-cross-module.exit create mode 100644 examples/expected/0791-modules-same-name-const-multi-level-cross-module.stderr create mode 100644 examples/expected/0791-modules-same-name-const-multi-level-cross-module.stdout create mode 100644 examples/expected/0792-modules-same-name-const-struct-field-dim.exit create mode 100644 examples/expected/0792-modules-same-name-const-struct-field-dim.stderr create mode 100644 examples/expected/0792-modules-same-name-const-struct-field-dim.stdout diff --git a/examples/expected/0786-modules-same-name-const-own.exit b/examples/expected/0786-modules-same-name-const-own.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0786-modules-same-name-const-own.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0786-modules-same-name-const-own.stderr b/examples/expected/0786-modules-same-name-const-own.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0786-modules-same-name-const-own.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0786-modules-same-name-const-own.stdout b/examples/expected/0786-modules-same-name-const-own.stdout new file mode 100644 index 0000000..b0ff4e7 --- /dev/null +++ b/examples/expected/0786-modules-same-name-const-own.stdout @@ -0,0 +1 @@ +a=1 b=2 diff --git a/examples/expected/0787-modules-same-name-const-ambiguous.exit b/examples/expected/0787-modules-same-name-const-ambiguous.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0787-modules-same-name-const-ambiguous.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0787-modules-same-name-const-ambiguous.stderr b/examples/expected/0787-modules-same-name-const-ambiguous.stderr new file mode 100644 index 0000000..39e505c --- /dev/null +++ b/examples/expected/0787-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/0787-modules-same-name-const-ambiguous.sx:12:21 + | +12 | print("K={}\n", K); + | ^ diff --git a/examples/expected/0787-modules-same-name-const-ambiguous.stdout b/examples/expected/0787-modules-same-name-const-ambiguous.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0787-modules-same-name-const-ambiguous.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/0788-modules-same-name-const-expr-chain-dim.exit b/examples/expected/0788-modules-same-name-const-expr-chain-dim.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0788-modules-same-name-const-expr-chain-dim.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0788-modules-same-name-const-expr-chain-dim.stderr b/examples/expected/0788-modules-same-name-const-expr-chain-dim.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0788-modules-same-name-const-expr-chain-dim.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0788-modules-same-name-const-expr-chain-dim.stdout b/examples/expected/0788-modules-same-name-const-expr-chain-dim.stdout new file mode 100644 index 0000000..6f78793 --- /dev/null +++ b/examples/expected/0788-modules-same-name-const-expr-chain-dim.stdout @@ -0,0 +1 @@ +a_len=2 a_val=2 b_len=11 b_val=11 diff --git a/examples/expected/0789-modules-same-name-const-leaf-author-pin.exit b/examples/expected/0789-modules-same-name-const-leaf-author-pin.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0789-modules-same-name-const-leaf-author-pin.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0789-modules-same-name-const-leaf-author-pin.stderr b/examples/expected/0789-modules-same-name-const-leaf-author-pin.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0789-modules-same-name-const-leaf-author-pin.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0789-modules-same-name-const-leaf-author-pin.stdout b/examples/expected/0789-modules-same-name-const-leaf-author-pin.stdout new file mode 100644 index 0000000..d5717e6 --- /dev/null +++ b/examples/expected/0789-modules-same-name-const-leaf-author-pin.stdout @@ -0,0 +1 @@ +val=2 len=2 diff --git a/examples/expected/0790-modules-same-name-const-cross-cycle-guard.exit b/examples/expected/0790-modules-same-name-const-cross-cycle-guard.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0790-modules-same-name-const-cross-cycle-guard.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0790-modules-same-name-const-cross-cycle-guard.stderr b/examples/expected/0790-modules-same-name-const-cross-cycle-guard.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0790-modules-same-name-const-cross-cycle-guard.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0790-modules-same-name-const-cross-cycle-guard.stdout b/examples/expected/0790-modules-same-name-const-cross-cycle-guard.stdout new file mode 100644 index 0000000..b2b8491 --- /dev/null +++ b/examples/expected/0790-modules-same-name-const-cross-cycle-guard.stdout @@ -0,0 +1 @@ +m=3 len=3 diff --git a/examples/expected/0791-modules-same-name-const-multi-level-cross-module.exit b/examples/expected/0791-modules-same-name-const-multi-level-cross-module.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0791-modules-same-name-const-multi-level-cross-module.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0791-modules-same-name-const-multi-level-cross-module.stderr b/examples/expected/0791-modules-same-name-const-multi-level-cross-module.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0791-modules-same-name-const-multi-level-cross-module.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0791-modules-same-name-const-multi-level-cross-module.stdout b/examples/expected/0791-modules-same-name-const-multi-level-cross-module.stdout new file mode 100644 index 0000000..ff8ad35 --- /dev/null +++ b/examples/expected/0791-modules-same-name-const-multi-level-cross-module.stdout @@ -0,0 +1 @@ +big=102 bk=11 diff --git a/examples/expected/0792-modules-same-name-const-struct-field-dim.exit b/examples/expected/0792-modules-same-name-const-struct-field-dim.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0792-modules-same-name-const-struct-field-dim.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0792-modules-same-name-const-struct-field-dim.stderr b/examples/expected/0792-modules-same-name-const-struct-field-dim.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0792-modules-same-name-const-struct-field-dim.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0792-modules-same-name-const-struct-field-dim.stdout b/examples/expected/0792-modules-same-name-const-struct-field-dim.stdout new file mode 100644 index 0000000..7ff3640 --- /dev/null +++ b/examples/expected/0792-modules-same-name-const-struct-field-dim.stdout @@ -0,0 +1 @@ +a_sz=2 b_sz=7 From 919c7bd8556ee11aba08234e022fcdc0a277cd8e Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 22:07:12 +0300 Subject: [PATCH 3/3] fix(stdlib/E5): source-aware value-const TYPE inference (F4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Value-const SELECTION was source-aware for emission/folding (F2/R1/F1), but expression TYPE inference still read the global last-wins `module_const_map`, so an inferred return type / coercion on a same-name const borrowed another module's const TYPE (mixed-type same-name consts were never exercised by the attempt-1 same-typed goldens). - expr_typer.zig: the `.identifier` const path now selects via the source-aware `selectModuleConst` (own-wins / one-flat-visible) instead of the global `module_const_map`. The global map still gates "is this a const name?"; an unpartitioned registration-only author emits its global type, and an ambiguous bare reference yields `.unresolved` (the emission path diagnoses loudly). - lower.zig: expose `selectModuleConst` so the type-inference path shares the one author selector emission/folding already use. Audited every `module_const_map` read: emission (4102) and global-init copy (1447) were already source-aware (attempt-1); the binds-a-value predicate (6400) is a boolean, not a type read; the in-`selectModuleConst` read (13842) is the unwired fallback. No sibling inference site leaks. examples: 0793 mixed-type own-wins inference (A's `K:s32` yields `1`, not the global `f64`'s `1.000000`); 0794 mixed-type bare → loud ambiguous (exit 1), the inference change does not mask the ambiguity. Prior E5 surfaces (0786-0792), the 0105 set (0752-0758), E1-E4 type surfaces (0763-0785) and FFI byte-identical; 533 markers green. --- .../0793-modules-same-name-const-type-infer.sx | 16 ++++++++++++++++ .../a.sx | 5 +++++ .../b.sx | 5 +++++ ...4-modules-same-name-const-type-ambiguous.sx | 15 +++++++++++++++ .../a.sx | 2 ++ .../b.sx | 2 ++ ...793-modules-same-name-const-type-infer.exit | 1 + ...3-modules-same-name-const-type-infer.stderr | 1 + ...3-modules-same-name-const-type-infer.stdout | 1 + ...modules-same-name-const-type-ambiguous.exit | 1 + ...dules-same-name-const-type-ambiguous.stderr | 5 +++++ ...dules-same-name-const-type-ambiguous.stdout | 1 + src/ir/expr_typer.zig | 18 +++++++++++++++--- src/ir/lower.zig | 2 +- 14 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 examples/0793-modules-same-name-const-type-infer.sx create mode 100644 examples/0793-modules-same-name-const-type-infer/a.sx create mode 100644 examples/0793-modules-same-name-const-type-infer/b.sx create mode 100644 examples/0794-modules-same-name-const-type-ambiguous.sx create mode 100644 examples/0794-modules-same-name-const-type-ambiguous/a.sx create mode 100644 examples/0794-modules-same-name-const-type-ambiguous/b.sx create mode 100644 examples/expected/0793-modules-same-name-const-type-infer.exit create mode 100644 examples/expected/0793-modules-same-name-const-type-infer.stderr create mode 100644 examples/expected/0793-modules-same-name-const-type-infer.stdout create mode 100644 examples/expected/0794-modules-same-name-const-type-ambiguous.exit create mode 100644 examples/expected/0794-modules-same-name-const-type-ambiguous.stderr create mode 100644 examples/expected/0794-modules-same-name-const-type-ambiguous.stdout diff --git a/examples/0793-modules-same-name-const-type-infer.sx b/examples/0793-modules-same-name-const-type-infer.sx new file mode 100644 index 0000000..f540f4e --- /dev/null +++ b/examples/0793-modules-same-name-const-type-infer.sx @@ -0,0 +1,16 @@ +// issue 0105 / F4 — same-name VALUE const TYPE inference is source-aware. Two +// flat-imported modules each declare a top-level `K` with a DIFFERENT declared +// TYPE (A: `s32`, B: `f64`) and an inferred-return function that reads `K` bare. +// The inferred return type must come from each module's OWN `K` (own-wins), not +// the global last-wins const TYPE: `a_k` infers A's `s32` and yields the integer +// `1` (printed `1`, not `1.000000`), while `b_k` infers B's `f64` and yields +// `2.500000`. Selection routes through the source-aware `selectModuleConst`, the +// same author selector emission/folding use — type inference must agree. +#import "modules/std.sx"; +#import "0793-modules-same-name-const-type-infer/a.sx"; +#import "0793-modules-same-name-const-type-infer/b.sx"; + +main :: () -> s32 { + print("a={} b={}\n", a_k(), b_k()); + 0 +} diff --git a/examples/0793-modules-same-name-const-type-infer/a.sx b/examples/0793-modules-same-name-const-type-infer/a.sx new file mode 100644 index 0000000..3d02899 --- /dev/null +++ b/examples/0793-modules-same-name-const-type-infer/a.sx @@ -0,0 +1,5 @@ +// Module A authors its OWN `K` declared `s32`. Its inferred-return `a_k` reads +// `K` bare; the inferred return type must be A's `s32`, so the value prints as +// the integer `1`, never coerced to B's `f64`. +K : s32 : 1; +a_k :: () { return K; } diff --git a/examples/0793-modules-same-name-const-type-infer/b.sx b/examples/0793-modules-same-name-const-type-infer/b.sx new file mode 100644 index 0000000..91b7bea --- /dev/null +++ b/examples/0793-modules-same-name-const-type-infer/b.sx @@ -0,0 +1,5 @@ +// Module B authors a DIFFERENT same-name `K` declared `f64` (a shadow of A's +// `K`). Its inferred-return `b_k` reads `K` bare; the inferred return type must +// be B's `f64`, so the value prints as `2.500000`. +K : f64 : 2.5; +b_k :: () { return K; } diff --git a/examples/0794-modules-same-name-const-type-ambiguous.sx b/examples/0794-modules-same-name-const-type-ambiguous.sx new file mode 100644 index 0000000..e5634d8 --- /dev/null +++ b/examples/0794-modules-same-name-const-type-ambiguous.sx @@ -0,0 +1,15 @@ +// issue 0105 / F4 — same-name VALUE const with DIFFERENT declared TYPES across +// two flat-imported modules (A: `s32`, B: `f64`), referenced bare at a mixed-type +// site. A bare `K` here is genuinely ambiguous — there are ≥2 flat-visible same- +// name authors — so it must diagnose loudly (exit 1), exactly as the same-typed +// 0787 does. The type-inference change must NOT mask the ambiguity (inferring +// `.unresolved` for the ambiguous reference) — the emission path still fires the +// loud author-count diagnostic regardless of the consts' declared types. +#import "modules/std.sx"; +#import "0794-modules-same-name-const-type-ambiguous/a.sx"; +#import "0794-modules-same-name-const-type-ambiguous/b.sx"; + +main :: () -> s32 { + print("K={}\n", K); + 0 +} diff --git a/examples/0794-modules-same-name-const-type-ambiguous/a.sx b/examples/0794-modules-same-name-const-type-ambiguous/a.sx new file mode 100644 index 0000000..a4460aa --- /dev/null +++ b/examples/0794-modules-same-name-const-type-ambiguous/a.sx @@ -0,0 +1,2 @@ +// Module A authors `K` declared `s32`. +K : s32 : 1; diff --git a/examples/0794-modules-same-name-const-type-ambiguous/b.sx b/examples/0794-modules-same-name-const-type-ambiguous/b.sx new file mode 100644 index 0000000..b713b4f --- /dev/null +++ b/examples/0794-modules-same-name-const-type-ambiguous/b.sx @@ -0,0 +1,2 @@ +// Module B authors a DIFFERENT same-name `K` declared `f64`. +K : f64 : 2.5; diff --git a/examples/expected/0793-modules-same-name-const-type-infer.exit b/examples/expected/0793-modules-same-name-const-type-infer.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0793-modules-same-name-const-type-infer.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0793-modules-same-name-const-type-infer.stderr b/examples/expected/0793-modules-same-name-const-type-infer.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0793-modules-same-name-const-type-infer.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0793-modules-same-name-const-type-infer.stdout b/examples/expected/0793-modules-same-name-const-type-infer.stdout new file mode 100644 index 0000000..ace952f --- /dev/null +++ b/examples/expected/0793-modules-same-name-const-type-infer.stdout @@ -0,0 +1 @@ +a=1 b=2.500000 diff --git a/examples/expected/0794-modules-same-name-const-type-ambiguous.exit b/examples/expected/0794-modules-same-name-const-type-ambiguous.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0794-modules-same-name-const-type-ambiguous.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0794-modules-same-name-const-type-ambiguous.stderr b/examples/expected/0794-modules-same-name-const-type-ambiguous.stderr new file mode 100644 index 0000000..671ec2f --- /dev/null +++ b/examples/expected/0794-modules-same-name-const-type-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/0794-modules-same-name-const-type-ambiguous.sx:13:21 + | +13 | print("K={}\n", K); + | ^ diff --git a/examples/expected/0794-modules-same-name-const-type-ambiguous.stdout b/examples/expected/0794-modules-same-name-const-type-ambiguous.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0794-modules-same-name-const-type-ambiguous.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index ed3d2f5..e7b5b70 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -277,9 +277,21 @@ pub const ExprTyper = struct { if (self.l.program_index.global_names.get(id.name)) |gi| { return gi.ty; } - // Check module-level value constants (e.g., WIDTH :f32: 800) - if (self.l.program_index.module_const_map.get(id.name)) |ci| { - return ci.ty; + // Check module-level value constants (e.g., WIDTH :f32: 800). + // F4: a same-name VALUE const must infer the SOURCE-AWARE author's + // TYPE (own-wins / one-flat-visible), not the global last-wins + // `module_const_map` — otherwise a return-type / coercion inferred + // on one module's `K` borrows another module's `K` TYPE. The global + // map still gates "is this a const name at all?"; `.none` is the + // registration-only author with no per-source partition (emit its + // global type), and an ambiguous bare reference yields `.unresolved` + // (the emission path diagnoses the ambiguity loudly). + if (self.l.program_index.module_const_map.get(id.name)) |ci_global| { + return switch (self.l.selectModuleConst(id.name)) { + .resolved => |sel| sel.info.ty, + .none => ci_global.ty, + .ambiguous => .unresolved, + }; } // A bare type name (alias like `Vec4`, struct name, or // builtin primitive) referenced in expression position diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 506ac87..c6093f5 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -13837,7 +13837,7 @@ pub const Lowering = struct { /// 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 { + pub 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;