fix(lang): numeric-limit shadow guard covers all 3 value sources [NL.2]
The issue-0092 fix guarded the numeric-limit accessor intercept against
raw value shadowing using only lexical Scope.lookup. The ordinary
identifier field-access path resolves a value through THREE sources
(scope / program_index.global_names / program_index.module_const_map),
so a backtick raw identifier bound at module scope — a global
`` `f64 := Box.{…} `` or a module constant `` `f64 :: Box.{…} `` — still
folded `` `f64.epsilon `` to the numeric limit instead of reading the
value's field (issue 0093, plus the module-const variant: same root
cause, same fix).
Fix: a single shared helper Lowering.identifierBindsValue(name) that
returns true when the name resolves through scope OR global_names OR
module_const_map. Used in BOTH lowerNumericLimit (lower.zig) and the
numeric-limit inference arm (expr_typer.zig) so the two resolvers can't
desync (issue-0083 class). A bare `f64.epsilon` / `s32.max` (a
.type_expr receiver) still folds even when a raw value of the same
spelling is bound — the bare receiver is never value-shadowed.
- examples/0161: extended to exercise all three binding kinds — a
GLOBAL `` `f32 ``, a MODULE-CONST `` `s16 ``, and LOCAL
`` `f64 ``/`` `s32 ``/`` `u8 `` — each reading its field while the
bare spelling still folds.
- src/ir/expr_typer.test.zig: unit test pinning the global +
module-const sources of the shared guard.
- issues/0093: RESOLVED banner (3-source root cause + fix, module-const
variant folded in).
- specs.md / readme.md: numeric-limit shadow note now source-agnostic
(local / global / module-const).
This commit is contained in:
@@ -118,3 +118,62 @@ test "expr_typer: raw value binding shadows numeric-limit, bare type still folds
|
||||
var type_fa = node(.{ .field_access = .{ .object = &type_recv, .field = "epsilon" } });
|
||||
try std.testing.expectEqual(TypeId.f64, l.inferExprType(&type_fa));
|
||||
}
|
||||
|
||||
// issue 0093: a raw value binding can shadow a builtin numeric type name through
|
||||
// any of three sources — lexical scope (issue 0092), program globals, or module
|
||||
// value constants. The shared `identifierBindsValue` guard consults all three,
|
||||
// so a global `` `f32 := Box.{…} `` and a module-const `` `s16 :: Box.{…} `` each
|
||||
// read the value's field (NOT the numeric-limit fold), while a bare `f32.max` /
|
||||
// `s16.max` (a `.type_expr` receiver) still folds. Pins the guard across the two
|
||||
// non-lexical sources the attempt-3 scope-only fix missed.
|
||||
test "expr_typer: global and module-const raw bindings shadow numeric-limit" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
|
||||
// `Box { max: s64 }` — the struct both raw bindings resolve to.
|
||||
const box_fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = module.types.internString("max"), .ty = .s64 },
|
||||
};
|
||||
const box_ty = module.types.intern(.{ .@"struct" = .{
|
||||
.name = module.types.internString("Box"),
|
||||
.fields = &box_fields,
|
||||
} });
|
||||
|
||||
// GLOBAL raw binding `` `f32 := Box.{…} `` — registered in global_names.
|
||||
try l.program_index.global_names.put("f32", .{ .id = @enumFromInt(0), .ty = box_ty });
|
||||
// MODULE-CONST raw binding `` `s16 :: Box.{…} `` — registered in module_const_map.
|
||||
var const_val = node(.{ .int_literal = .{ .value = 0 } });
|
||||
try l.program_index.module_const_map.put("s16", .{ .value = &const_val, .ty = box_ty });
|
||||
|
||||
// The shared guard sees both non-lexical bindings, but not an unbound spelling.
|
||||
try std.testing.expect(l.identifierBindsValue("f32"));
|
||||
try std.testing.expect(l.identifierBindsValue("s16"));
|
||||
try std.testing.expect(!l.identifierBindsValue("u8"));
|
||||
|
||||
// `` `f32.max `` — global raw receiver → ordinary field read, types as s64
|
||||
// (the field), not f32 (the fold).
|
||||
var g_recv = node(.{ .identifier = .{ .name = "f32", .is_raw = true } });
|
||||
var g_fa = node(.{ .field_access = .{ .object = &g_recv, .field = "max" } });
|
||||
try std.testing.expectEqual(TypeId.s64, l.inferExprType(&g_fa));
|
||||
|
||||
// `` `s16.max `` — module-const raw receiver → ordinary field read, types as s64.
|
||||
var c_recv = node(.{ .identifier = .{ .name = "s16", .is_raw = true } });
|
||||
var c_fa = node(.{ .field_access = .{ .object = &c_recv, .field = "max" } });
|
||||
try std.testing.expectEqual(TypeId.s64, l.inferExprType(&c_fa));
|
||||
|
||||
// bare `f32.max` — type_expr receiver, never shadowed → folds to f32, even
|
||||
// though a global `` `f32 `` value is bound.
|
||||
var bare_f32 = node(.{ .type_expr = .{ .name = "f32" } });
|
||||
var bare_f32_fa = node(.{ .field_access = .{ .object = &bare_f32, .field = "max" } });
|
||||
try std.testing.expectEqual(TypeId.f32, l.inferExprType(&bare_f32_fa));
|
||||
|
||||
// bare `s16.max` — type_expr receiver, never shadowed → folds to the s16
|
||||
// type, even though a module-const `` `s16 `` value is bound.
|
||||
var bare_s16 = node(.{ .type_expr = .{ .name = "s16" } });
|
||||
var bare_s16_fa = node(.{ .field_access = .{ .object = &bare_s16, .field = "max" } });
|
||||
try std.testing.expectEqual(TypeId.s16, l.inferExprType(&bare_s16_fa));
|
||||
}
|
||||
|
||||
@@ -143,9 +143,11 @@ pub const ExprTyper = struct {
|
||||
// Skip the fold when a raw value binding shadows the
|
||||
// builtin type name (`` `f64 := … ``) — mirrors the
|
||||
// lowerNumericLimit guard so inference matches lowering
|
||||
// (issue 0092). A `.type_expr` receiver is never shadowed.
|
||||
// (issues 0092, 0093). The shared helper consults all
|
||||
// three value sources (scope / globals / module consts);
|
||||
// a `.type_expr` receiver is never shadowed.
|
||||
const shadowed = fa.object.data == .identifier and
|
||||
(if (self.l.scope) |scope| (scope.lookup(tn) != null) else false);
|
||||
self.l.identifierBindsValue(tn);
|
||||
if (!shadowed and
|
||||
(TypeResolver.integerLimitFor(tn, fa.field) != null or
|
||||
TypeResolver.floatLimitFor(tn, fa.field) != null))
|
||||
|
||||
@@ -4904,6 +4904,27 @@ pub const Lowering = struct {
|
||||
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
|
||||
}
|
||||
|
||||
/// True when an `.identifier` receiver text resolves to an in-scope VALUE
|
||||
/// binding rather than a builtin type. A backtick raw identifier (F0.6) can
|
||||
/// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``);
|
||||
/// such a value is reachable through the same three sources the ordinary
|
||||
/// identifier field-access path consults (see `expr_typer` `.identifier`
|
||||
/// arm): lexical `scope`, program `global_names`, and module value
|
||||
/// constants `module_const_map`. The numeric-limit intercept must defer to
|
||||
/// ordinary field access whenever ANY of the three binds the name, so a
|
||||
/// raw value field read is never hijacked into a numeric-limit fold
|
||||
/// (issues 0092 local / 0093 global + module-const). A single helper used
|
||||
/// by both lowering and inference keeps the two resolvers in lockstep
|
||||
/// (issue-0083 two-resolver defect class).
|
||||
pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool {
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(name) != null) return true;
|
||||
}
|
||||
if (self.program_index.global_names.get(name) != null) return true;
|
||||
if (self.program_index.module_const_map.get(name) != null) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Numeric-limit accessor intercept (`<Type>.min`/`.max`/`.epsilon`/
|
||||
/// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` /
|
||||
/// `Struct.CONST` / pack-arity identifier-receiver intercepts in
|
||||
@@ -4933,13 +4954,10 @@ pub const Lowering = struct {
|
||||
// shadows a builtin type name (`` `f64 := … ``). Field access on that
|
||||
// value is an ordinary field read, not a numeric-limit fold — defer to
|
||||
// the normal field-access path when the receiver identifier resolves to
|
||||
// an in-scope value binding (issue 0092). A `.type_expr` receiver is
|
||||
// unambiguously a type and can never be value-shadowed.
|
||||
if (fa.object.data == .identifier) {
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(name) != null) return null;
|
||||
}
|
||||
}
|
||||
// a value binding through any of scope / globals / module consts
|
||||
// (issues 0092, 0093). A `.type_expr` receiver is unambiguously a type
|
||||
// and can never be value-shadowed.
|
||||
if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null;
|
||||
|
||||
if (TypeResolver.integerLimitFor(name, fa.field)) |value| {
|
||||
return self.builder.constInt(value, ty);
|
||||
|
||||
Reference in New Issue
Block a user