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:
agra
2026-06-04 23:59:11 +03:00
parent 463557990f
commit b0cc22a8c0
10 changed files with 217 additions and 3 deletions

View File

@@ -0,0 +1,50 @@
// Numeric-limit accessor vs. a raw value binding that shadows a builtin type
// name. A backtick raw identifier (F0.6) can legitimately bind a value whose
// spelling is a reserved numeric type name (`` `f64 ``, `` `s32 ``, `` `u8 ``).
// Field access on such a value is an ORDINARY field read — the numeric-limit
// intercept (NL.1 integer `.min`/`.max`, NL.2 float `.epsilon`/… ) must NOT
// hijack it. An adjacent BARE `f64.epsilon` / `s32.max` / `u8.max` — which the
// parser classifies as a type receiver, not the raw value — STILL folds to the
// numeric limit. Both behaviors coexist in one scope: the raw receiver reads
// the value, the bare receiver folds the limit.
//
// Regression (issue 0092): the intercept previously treated any identifier
// whose text matched a builtin numeric type name as a TYPE receiver, silently
// shadowing the in-scope value binding (`` `f64.epsilon `` folded to 2^-52,
// `` `s32.max `` folded to 2147483647 — a silent wrong value).
#import "modules/std.sx";
FBox :: struct { epsilon: s64; max: s64; min_positive: s64; }
IBox :: struct { max: s64; min: s64; }
UBox :: struct { max: s64; }
main :: () -> s32 {
// Raw value bindings whose spelling shadows a builtin numeric type name.
`f64 := FBox.{ epsilon = 11, max = 22, min_positive = 33 };
`s32 := IBox.{ max = 78, min = -78 };
`u8 := UBox.{ max = 7 };
// Raw receiver → ordinary field READ (the value), never the numeric limit.
print("val f64: epsilon={} max={} min_positive={}\n",
`f64.epsilon, `f64.max, `f64.min_positive); // 11 22 33
print("val s32: max={} min={}\n", `s32.max, `s32.min); // 78 -78
print("val u8: max={}\n", `u8.max); // 7
// The value-field read carries the field type (s64 here): round-trips
// through a typed binding, so a mistyped/boxed read would not type-check.
e : s64 = `f64.epsilon;
print("typed val e={}\n", e); // 11
// Bare receiver (a type receiver, NOT the raw value) → STILL folds to the
// numeric limit, even though `s32`/`u8`/`f64` are bound in this same scope.
print("lim s32.max={} s32.min={}\n", s32.max, s32.min); // 2147483647 -2147483648
print("lim u8.max={}\n", u8.max); // 255
// Bare float accessors still fold; the formatter is crude (issue 0090), so
// pin the values by their defining properties rather than by printing.
print("lim (1.0+f64.epsilon)!=1.0: {}\n", (1.0 + f64.epsilon) != 1.0); // true
print("lim f64.inf > f64.max: {}\n", f64.inf > f64.max); // true
print("lim f64.min == -f64.max: {}\n", f64.min == -f64.max); // true
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,9 @@
val f64: epsilon=11 max=22 min_positive=33
val s32: max=78 min=-78
val u8: max=7
typed val e=11
lim s32.max=2147483647 s32.min=-2147483648
lim u8.max=255
lim (1.0+f64.epsilon)!=1.0: true
lim f64.inf > f64.max: true
lim f64.min == -f64.max: true