diff --git a/examples/0761-modules-same-name-const-expr-chain-dim.sx b/examples/0761-modules-same-name-const-expr-chain-dim.sx new file mode 100644 index 0000000..d635b38 --- /dev/null +++ b/examples/0761-modules-same-name-const-expr-chain-dim.sx @@ -0,0 +1,19 @@ +// issue 0105 / F2 — 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`. Pre-fix (72f06a1) the dimension fold +// recursed through the global map, so `a_len` read B's `M` (= 11) while `a_val` +// correctly read A's chain (= 2) — an INCOHERENCE for the same const `K`. Now +// both observables agree per module: a_len=2 a_val=2, b_len=11 b_val=11. +#import "modules/std.sx"; +#import "0761-modules-same-name-const-expr-chain-dim/a.sx"; +#import "0761-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/0761-modules-same-name-const-expr-chain-dim/a.sx b/examples/0761-modules-same-name-const-expr-chain-dim/a.sx new file mode 100644 index 0000000..a806828 --- /dev/null +++ b/examples/0761-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/0761-modules-same-name-const-expr-chain-dim/b.sx b/examples/0761-modules-same-name-const-expr-chain-dim/b.sx new file mode 100644 index 0000000..db1c9a1 --- /dev/null +++ b/examples/0761-modules-same-name-const-expr-chain-dim/b.sx @@ -0,0 +1,11 @@ +// Module B authors a DIFFERENT same-name chain: `M :: 10`, `K :: M + 1` (= 11). +// A shadow of A's `M`/`K`. Pre-fix the dimension fold collapsed to B's `M` for +// BOTH modules via the global last-wins map; now each `K`'s RHS leaf resolves to +// its own source's `M`. +M :: 10; +K :: M + 1; +b_val :: () -> s64 { return K; } +b_len :: () -> s64 { + arr : [K]u8 = ---; + return arr.len; +} diff --git a/examples/expected/0761-modules-same-name-const-expr-chain-dim.exit b/examples/expected/0761-modules-same-name-const-expr-chain-dim.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0761-modules-same-name-const-expr-chain-dim.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0761-modules-same-name-const-expr-chain-dim.stderr b/examples/expected/0761-modules-same-name-const-expr-chain-dim.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0761-modules-same-name-const-expr-chain-dim.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0761-modules-same-name-const-expr-chain-dim.stdout b/examples/expected/0761-modules-same-name-const-expr-chain-dim.stdout new file mode 100644 index 0000000..6f78793 --- /dev/null +++ b/examples/expected/0761-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/src/ir/lower.zig b/src/ir/lower.zig index 47ec15d..58ea432 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -54,6 +54,51 @@ 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`). A name 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, + parent: ?*const ConstFoldFrame, +}; + +fn constFoldFrameContains(frame: ?*const ConstFoldFrame, name: []const u8) bool { + var cur = frame; + while (cur) |c| : (cur = c.parent) { + if (std.mem.eql(u8, c.name, name)) return true; + } + return false; +} + +/// Folding context for a SOURCE-AWARE module-const EXPRESSION RHS (E2/F2). +/// 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 { @@ -13411,8 +13456,22 @@ 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 { + return self.foldSourceConstFloat(name, null); + } + + /// Source-aware FLOAT fold of a module const `name` (E2/F2): the SELECTED + /// author's RHS is folded with nested leaves resolved SOURCE-AWARE (own-wins) + /// rather than through the global last-wins map. `frame` cycle-guards a const + /// whose value references another const. Single-author → byte-identical to + /// the legacy global-map fold (the selected `ci` IS the global one). + fn foldSourceConstFloat(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?f64 { + if (constFoldFrameContains(frame, name)) return null; return switch (self.selectModuleConst(name)) { - .resolved => |ci| program_index_mod.moduleConstFloatWith(&self.program_index.module_const_map, &self.module.types, name, ci), + .resolved => |ci| { + if (!program_index_mod.isCountableConstType(&self.module.types, ci.ty)) return null; + var f = ConstFoldFrame{ .name = name, .parent = frame }; + return program_index_mod.evalConstFloatExpr(ci.value, SourceConstCtx{ .lowering = self, .frame = &f }); + }, .ambiguous, .none => null, }; } @@ -13424,13 +13483,28 @@ 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 { + return self.sourceConstIsFloatTyped(name, null); + } + + /// Source-aware "is `name` a FLOAT-valued module const" (E2/F2): judge the + /// SELECTED author's value, with nested const leaves resolved source-aware. + /// `frame` cycle-guards a const whose value references another const. + fn sourceConstIsFloatTyped(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) bool { + if (constFoldFrameContains(frame, name)) return false; return switch (self.selectModuleConst(name)) { - .resolved => |ci| program_index_mod.moduleConstIsFloatTypedWith(&self.program_index.module_const_map, &self.module.types, name, ci), + .resolved => |ci| { + if (program_index_mod.isFloatConstType(ci.ty)) return true; + var f = ConstFoldFrame{ .name = name, .parent = frame }; + return program_index_mod.isFloatValuedExpr(ci.value, SourceConstCtx{ .lowering = self, .frame = &f }); + }, .ambiguous, .none => false, }; } /// 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, @@ -13439,14 +13513,28 @@ pub const Lowering = struct { if (self.comptime_value_bindings) |cvb| { if (cvb.get(name)) |v| return v; } - // E2/F2: select the SOURCE-AWARE module-const author (own-wins; ≥2 - // flat-visible → ambiguous → no value here, the loud diagnostic is the - // reference site's job). The selected `ci`'s RHS is folded over the global - // leaf map, so a const-EXPRESSION chain (`N :: M + 1`, M flat-imported) - // still resolves `M` exactly as before. Single-author selects the same - // `ci` the global map holds, so this is byte-identical to the legacy read. + return self.foldSourceConstInt(name, null); + } + + /// Source-aware INTEGER fold of a module const `name` (E2/F2). The crux of + /// F2's expression-chain fix: 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` — i.e. 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` + /// 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 { + if (constFoldFrameContains(frame, name)) return null; return switch (self.selectModuleConst(name)) { - .resolved => |ci| program_index_mod.moduleConstIntWith(&self.program_index.module_const_map, &self.module.types, name, ci), + .resolved => |ci| { + if (!program_index_mod.isCountableConstType(&self.module.types, ci.ty)) return null; + var f = ConstFoldFrame{ .name = name, .parent = frame }; + return program_index_mod.evalConstIntExpr(ci.value, SourceConstCtx{ .lowering = self, .frame = &f }); + }, .ambiguous, .none => null, }; } diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 3eb8957..c0c847c 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -118,7 +118,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; } @@ -152,7 +152,7 @@ fn foldConstFloatValued(consts: *const std.StringHashMap(ModuleConstInfo), table /// 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)) { @@ -177,15 +177,6 @@ fn foldConstInt(consts: *const std.StringHashMap(ModuleConstInfo), table: *const return evalConstIntExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame }); } -/// `moduleConstInt` for a const whose AUTHOR was already selected source-aware -/// (E2): fold the provided `ci`'s RHS instead of `consts.get(name)`, so a -/// same-name shadow resolves to ITS OWN value while a const-EXPRESSION leaf still -/// resolves through the global `consts` map (the chain reader). Single-author → -/// byte-identical to `moduleConstInt` (the selected `ci` IS the global one). -pub fn moduleConstIntWith(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo) ?i64 { - return foldConstInt(consts, table, name, ci, null); -} - /// A name bound to a module-global integer constant → its value, else null. /// SINGLE source for both array-dimension resolvers — the stateful /// body-lowering path (`Lowering.comptimeIntNamed`) and the stateless @@ -230,12 +221,6 @@ pub fn moduleConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table return moduleConstFloatFramed(consts, table, name, null); } -/// `moduleConstFloat` for a source-aware-selected `ci` (E2) — the float analog -/// of `moduleConstIntWith`. -pub fn moduleConstFloatWith(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo) ?f64 { - return foldConstFloat(consts, table, name, ci, null); -} - /// True iff `name` is a FLOAT-valued module const — judged by VALUE, so it covers /// a typed float const (`K : f64 : 4.0`), an untyped float-EXPRESSION const /// (`ME :: 4.0 + 1.0`, whose placeholder type is `s64`), and a non-integral float @@ -246,11 +231,6 @@ pub fn moduleConstIsFloatTyped(consts: *const std.StringHashMap(ModuleConstInfo) return moduleConstFloatValuedFramed(consts, table, name, null); } -/// `moduleConstIsFloatTyped` for a source-aware-selected `ci` (E2). -pub fn moduleConstIsFloatTypedWith(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, ci: ModuleConstInfo) bool { - return foldConstFloatValued(consts, table, name, ci, null); -} - /// True iff `node` is a FLOAT-valued compile-time expression — a float literal, /// a float-typed const leaf (`F : f64 : 2.5`, `K : f64 : 4.0`), a builtin float /// numeric-limit (`f64.max`), or arithmetic over any of those. THE predicate the