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:
@@ -5,30 +5,52 @@
|
||||
// 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.
|
||||
// numeric limit. Both behaviors coexist: 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).
|
||||
// A raw value binding can reach the intercept through THREE sources, exactly
|
||||
// mirroring the ordinary identifier field-access path (scope / globals / module
|
||||
// consts). This example exercises all three: a GLOBAL `` `f32 ``, a MODULE-CONST
|
||||
// `` `s16 ``, and LOCAL `` `f64 ``/`` `s32 ``/`` `u8 `` — each reads its field,
|
||||
// and the bare spelling of each STILL folds.
|
||||
//
|
||||
// Regression (issues 0092 local, 0093 global + module-const): 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). The attempt-3 fix guarded only lexical scope, so
|
||||
// GLOBAL and MODULE-CONST raw bindings still folded (issue 0093).
|
||||
#import "modules/std.sx";
|
||||
|
||||
FBox :: struct { epsilon: s64; max: s64; min_positive: s64; }
|
||||
IBox :: struct { max: s64; min: s64; }
|
||||
UBox :: struct { max: s64; }
|
||||
|
||||
// GLOBAL raw value binding whose spelling shadows the builtin `f32`. Reachable
|
||||
// via `program_index.global_names`, not lexical scope (issue 0093).
|
||||
`f32 := FBox.{ epsilon = 44, max = 55, min_positive = 66 };
|
||||
|
||||
// MODULE-CONST raw value binding whose spelling shadows the builtin `s16`.
|
||||
// Reachable via `program_index.module_const_map` (issue 0093, const variant).
|
||||
`s16 :: IBox.{ max = 99, min = -99 };
|
||||
|
||||
main :: () -> s32 {
|
||||
// Raw value bindings whose spelling shadows a builtin numeric type name.
|
||||
// LOCAL 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
|
||||
print("local f64: epsilon={} max={} min_positive={}\n",
|
||||
`f64.epsilon, `f64.max, `f64.min_positive); // 11 22 33
|
||||
print("local s32: max={} min={}\n", `s32.max, `s32.min); // 78 -78
|
||||
print("local u8: max={}\n", `u8.max); // 7
|
||||
|
||||
// GLOBAL raw receiver → ordinary field READ (issue 0093).
|
||||
print("global f32: epsilon={} max={} min_positive={}\n",
|
||||
`f32.epsilon, `f32.max, `f32.min_positive); // 44 55 66
|
||||
// MODULE-CONST raw receiver → ordinary field READ (issue 0093).
|
||||
print("const s16: max={} min={}\n", `s16.max, `s16.min); // 99 -99
|
||||
|
||||
// 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.
|
||||
@@ -36,15 +58,19 @@ main :: () -> s32 {
|
||||
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.
|
||||
// numeric limit, even though a LOCAL (`s32`/`u8`/`f64`), GLOBAL (`f32`), or
|
||||
// MODULE-CONST (`s16`) value of the same spelling is bound. The bare receiver
|
||||
// is never blocked by any of the three value sources.
|
||||
print("lim s32.max={} s32.min={}\n", s32.max, s32.min); // 2147483647 -2147483648
|
||||
print("lim u8.max={}\n", u8.max); // 255
|
||||
print("lim s16.max={} s16.min={}\n", s16.max, s16.min); // 32767 -32768
|
||||
|
||||
// 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
|
||||
print("lim f32.inf > f32.max: {}\n", f32.inf > f32.max); // true
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
val f64: epsilon=11 max=22 min_positive=33
|
||||
val s32: max=78 min=-78
|
||||
val u8: max=7
|
||||
local f64: epsilon=11 max=22 min_positive=33
|
||||
local s32: max=78 min=-78
|
||||
local u8: max=7
|
||||
global f32: epsilon=44 max=55 min_positive=66
|
||||
const s16: max=99 min=-99
|
||||
typed val e=11
|
||||
lim s32.max=2147483647 s32.min=-2147483648
|
||||
lim u8.max=255
|
||||
lim s16.max=32767 s16.min=-32768
|
||||
lim (1.0+f64.epsilon)!=1.0: true
|
||||
lim f64.inf > f64.max: true
|
||||
lim f64.min == -f64.max: true
|
||||
lim f32.inf > f32.max: true
|
||||
|
||||
Reference in New Issue
Block a user