fix(lang): numeric-limit intercept no longer shadows raw value bindings [NL.2]
The numeric-limit accessor intercept (NL.1 integer `.min`/`.max`, NL.2 float `.epsilon`/`.min_positive`/`.true_min`/`.inf`/`.nan`) treated ANY receiver whose text matched a builtin numeric type name as a TYPE receiver, without first checking for an in-scope VALUE binding. An F0.6 backtick raw identifier (`` `f64 := … ``) binds a local under the stripped name `f64`; field access on it (`` `f64.epsilon ``) parses as an `.identifier` receiver, which the intercept silently folded to the type's numeric limit — a silent-wrong-value bug (issue 0092). Fix: for `.identifier` receivers, prefer an in-scope value binding (`Scope.lookup`) over the fold — defer to ordinary field lowering when the identifier resolves to a value. `.type_expr` receivers are unambiguous types and are never shadowed, so a bare `f64.epsilon`/`s32.max` still folds even in a scope where `` `f64 `` is bound (the parser classifies a bare builtin name as a `.type_expr`). Mirrored in expr_typer.zig so inference matches lowering (avoids the issue-0083 two-resolver desync). Float-only-on-int and non-numeric-receiver errors are unchanged. - src/ir/lower.zig: value-binding guard in lowerNumericLimit. - src/ir/expr_typer.zig: same guard in the numeric-limit inference arm. - src/ir/expr_typer.test.zig: unit test pinning the two-resolver agreement. - examples/0161-types-numeric-limit-value-shadow.sx: regression — raw `` `f64 ``/`` `s32 ``/`` `u8 `` value reads coexisting with bare folds. - issues/0092: RESOLVED banner. - specs.md / readme.md: receiver-vs-shadowing-value-binding note.
This commit is contained in:
@@ -9,7 +9,9 @@ const Node = ast.Node;
|
||||
|
||||
const ir_mod = @import("ir.zig");
|
||||
const TypeId = ir_mod.TypeId;
|
||||
const Ref = ir_mod.Ref;
|
||||
const Lowering = ir_mod.Lowering;
|
||||
const Scope = @import("lower.zig").Scope;
|
||||
|
||||
fn node(data: ast.Node.Data) Node {
|
||||
return .{ .span = .{ .start = 0, .end = 0 }, .data = data };
|
||||
@@ -73,3 +75,46 @@ test "expr_typer: deref of a non-pointer is unresolved" {
|
||||
var deref_n = node(.{ .deref_expr = .{ .operand = &i } });
|
||||
try std.testing.expectEqual(TypeId.unresolved, l.inferExprType(&deref_n));
|
||||
}
|
||||
|
||||
// issue 0092: a raw `` `f64 `` value binding shadows the builtin numeric type
|
||||
// name — `` `f64.epsilon `` (an `.identifier` receiver) must type as the value's
|
||||
// field, NOT the float numeric-limit fold. A bare `f64.epsilon` (a `.type_expr`
|
||||
// receiver, never shadowed) still folds to the queried float type. This pins the
|
||||
// two-resolver agreement: expr_typer's inference must match lowerNumericLimit.
|
||||
test "expr_typer: raw value binding shadows numeric-limit, bare type still folds" {
|
||||
// Arena-backed (like calls.test.zig's scope tests): the `.identifier`
|
||||
// field-access path interns a `obj.field` probe string into `l.alloc`,
|
||||
// which the production compiler owns via an arena — an explicit free would
|
||||
// not match the real lifetime.
|
||||
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);
|
||||
|
||||
// A struct `Box { epsilon: s64 }` bound to the raw name `` `f64 ``.
|
||||
const box_fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = module.types.internString("epsilon"), .ty = .s64 },
|
||||
};
|
||||
const box_ty = module.types.intern(.{ .@"struct" = .{
|
||||
.name = module.types.internString("Box"),
|
||||
.fields = &box_fields,
|
||||
} });
|
||||
|
||||
var scope = Scope.init(alloc, null);
|
||||
defer scope.deinit();
|
||||
l.scope = &scope;
|
||||
scope.put("f64", .{ .ref = Ref.none, .ty = box_ty, .is_alloca = false });
|
||||
|
||||
// `` `f64.epsilon `` — identifier receiver resolving to the value binding →
|
||||
// ordinary field read, types as s64 (the field), not f64.
|
||||
var raw_recv = node(.{ .identifier = .{ .name = "f64", .is_raw = true } });
|
||||
var raw_fa = node(.{ .field_access = .{ .object = &raw_recv, .field = "epsilon" } });
|
||||
try std.testing.expectEqual(TypeId.s64, l.inferExprType(&raw_fa));
|
||||
|
||||
// bare `f64.epsilon` — type_expr receiver, never shadowed → folds to f64.
|
||||
var type_recv = node(.{ .type_expr = .{ .name = "f64" } });
|
||||
var type_fa = node(.{ .field_access = .{ .object = &type_recv, .field = "epsilon" } });
|
||||
try std.testing.expectEqual(TypeId.f64, l.inferExprType(&type_fa));
|
||||
}
|
||||
|
||||
@@ -140,8 +140,15 @@ pub const ExprTyper = struct {
|
||||
else => null,
|
||||
};
|
||||
if (type_name) |tn| {
|
||||
if (TypeResolver.integerLimitFor(tn, fa.field) != null or
|
||||
TypeResolver.floatLimitFor(tn, fa.field) != null)
|
||||
// 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.
|
||||
const shadowed = fa.object.data == .identifier and
|
||||
(if (self.l.scope) |scope| (scope.lookup(tn) != null) else false);
|
||||
if (!shadowed and
|
||||
(TypeResolver.integerLimitFor(tn, fa.field) != null or
|
||||
TypeResolver.floatLimitFor(tn, fa.field) != null))
|
||||
{
|
||||
if (TypeResolver.resolveBuiltinName(tn, &self.l.module.types)) |t| return t;
|
||||
}
|
||||
|
||||
@@ -4928,6 +4928,19 @@ pub const Lowering = struct {
|
||||
};
|
||||
if (!TypeResolver.isLimitField(fa.field)) return null;
|
||||
const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null;
|
||||
|
||||
// A backtick raw identifier (F0.6) can bind a value whose spelling
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (TypeResolver.integerLimitFor(name, fa.field)) |value| {
|
||||
return self.builder.constInt(value, ty);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user