fix(lower): propagate source-aware const selection into expression-chain folds — close F2 R1 [stdlib E2 attempt-4]

attempt-3 made the value-const READ source-aware (own-wins / ambiguous) but
the dimension/count fold of a SELECTED const's RHS still recursed through the
global last-wins `module_const_map`, so a nested same-name leaf came from the
wrong module. Reviewer R1: a.sx `M::1; K::M+1`, b.sx `M::10; K::M+1`, with both
`[K]u8` (a_len) and `return K` (a_val) — pre-fix `a_len=11 a_val=2`, an
INCOHERENCE for the same const `K` (a_val read A's chain; a_len read B's `M`).

`comptimeIntNamed` delegated to `moduleConstIntWith(global_map, ...)`, whose
leaf ctx (`ModuleConstCtx`) resolved nested names through the global map. The
value path (`emitModuleConst` -> `foldCountI64(ci.value, self)`) folds through
`self`, so its leaves bounce back to the source-aware `comptimeIntNamed` — which
is why a_val was already correct.

- New `SourceConstCtx` (lower.zig): the leaf-resolution twin of `ModuleConstCtx`,
  but every nested const leaf re-selects its OWN source author via
  `selectModuleConst` (own-wins / ambiguous), never the global last-wins map.
  `ConstFoldFrame` cycle-guards a const whose RHS references another const.
- `comptimeIntNamed` / `lookupFloatName` / `nameIsFloatTyped` now fold the
  selected `ci`'s RHS through `SourceConstCtx` (via `foldSourceConstInt` /
  `foldSourceConstFloat` / `sourceConstIsFloatTyped`). This makes the dimension
  and value reads of a shadowed expression-chain const coherent.
- Drop the now-unused `moduleConst{Int,Float,IsFloatTyped}With` wrappers from
  program_index.zig; expose `isCountableConstType` / `isFloatConstType`.

Single-author -> byte-identical (the selected `ci` IS the global one and every
nested leaf has one author). The stateless `type_bridge` registration-time const
reader still folds leaves through the global map, but realistic dim sites (struct
fields, array aliases — probed) resolve via the stateful path and stay coherent
under import-order swaps; no reachable wrong-dimension found (tracked follow-up,
byte-identical single-author).

Regression: examples/0761-modules-same-name-const-expr-chain-dim — a_len=2
a_val=2, b_len=11 b_val=11. Fail-before on 72f06a1 (`a_len=11`), pass-after.

Gate: zig build + zig build test (423/423, LSP sweep 515 clean) + run_examples
(499/0, 498 prior byte-identical + 0761) + m3te ios-sim build exit 0.
This commit is contained in:
agra
2026-06-08 01:06:44 +03:00
parent 72f06a109b
commit 8518b66cec
8 changed files with 141 additions and 31 deletions

View File

@@ -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,
};
}

View File

@@ -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