Merge branch 'flow/sx-foundation/NL.2' into dist-foundation
This commit is contained in:
94
examples/0159-types-float-numeric-limits.sx
Normal file
94
examples/0159-types-float-numeric-limits.sx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Float numeric-limit accessors: `f32`/`f64` expose `.min` / `.max` (sibling of
|
||||||
|
// the integer `.min`/`.max`, NL.1) plus the float-only `.epsilon`,
|
||||||
|
// `.min_positive`, `.true_min`, `.inf`, and `.nan`. Each folds, at compile time,
|
||||||
|
// to a constant of the QUERIED float type via the same `lowerNumericLimit`
|
||||||
|
// intercept as the integer case (`builder.constFloat` + the `std.math`
|
||||||
|
// constants), driven by `TypeResolver.floatLimitFor`.
|
||||||
|
//
|
||||||
|
// The lexer has no exponent notation and the default float formatter is crude
|
||||||
|
// (issue 0090), so these limits can be pinned NEITHER by literal comparison NOR
|
||||||
|
// by printing. Every accessor is asserted instead by reinterpreting its bits
|
||||||
|
// through an untagged union and comparing against the exact IEEE-754 hex
|
||||||
|
// pattern — plus the defining-property checks that no other value could satisfy.
|
||||||
|
//
|
||||||
|
// Semantics (Agra-ruled, consistent with the integer accessors):
|
||||||
|
// .min = most-NEGATIVE finite (= -max), NOT C's DBL_MIN
|
||||||
|
// .max = largest finite
|
||||||
|
// .epsilon = ULP of 1.0 (next f after 1.0 minus 1.0), NOT C#'s denormal Epsilon
|
||||||
|
// .min_positive = smallest positive NORMAL (= C DBL_MIN / Rust MIN_POSITIVE)
|
||||||
|
// .true_min = smallest positive SUBNORMAL (next value above 0.0)
|
||||||
|
// .inf = +infinity
|
||||||
|
// .nan = a quiet NaN
|
||||||
|
//
|
||||||
|
// Regression (issue 0091): `f64.nan != f64.nan` is true — native float `!=`
|
||||||
|
// lowers UNORDERED, so a NaN compares unequal to everything including itself.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
// `bits` mirrors each float's raw IEEE-754 storage. f64 needs 64 bits, f32 32.
|
||||||
|
// The f64 union's `bits` (u64) view reads the all-ones-ish positive patterns as
|
||||||
|
// their true magnitude; its `s` (s64) view pins the negative `f64.min` pattern
|
||||||
|
// (0xFFEF…), whose unsigned form overflows the u64 literal parser, by comparing
|
||||||
|
// the signed reinterpret to -4503599627370497.
|
||||||
|
Uf64 :: union { f: f64; bits: u64; s: s64; }
|
||||||
|
Uf32 :: union { f: f32; bits: u32; }
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
o : Uf64 = ---;
|
||||||
|
|
||||||
|
// Read `.true_min` (a subnormal) FIRST and through the union only — never via
|
||||||
|
// arithmetic. Under flush-to-zero / denormals-are-zero CPU modes a subnormal
|
||||||
|
// can flush to 0.0 on the first arithmetic op, so the bit reinterpret is the
|
||||||
|
// only reliable channel for it.
|
||||||
|
o.f = f64.true_min;
|
||||||
|
print("f64.true_min {}\n", o.bits == 0x0000000000000001); // true
|
||||||
|
|
||||||
|
o.f = f64.max;
|
||||||
|
print("f64.max {}\n", o.bits == 0x7FEFFFFFFFFFFFFF); // true
|
||||||
|
// f64.min = -max; its bit pattern 0xFFEFFFFFFFFFFFFF overflows an unsigned u64
|
||||||
|
// literal, so it is pinned directly via the SIGNED s64 view: -4503599627370497.
|
||||||
|
o.f = f64.min;
|
||||||
|
print("f64.min {}\n", o.s == -4503599627370497); // true (bits 0xFFEFFFFFFFFFFFFF)
|
||||||
|
o.f = f64.epsilon;
|
||||||
|
print("f64.epsilon {}\n", o.bits == 0x3CB0000000000000); // true
|
||||||
|
o.f = f64.min_positive;
|
||||||
|
print("f64.min_positive {}\n", o.bits == 0x0010000000000000); // true
|
||||||
|
o.f = f64.inf;
|
||||||
|
print("f64.inf {}\n", o.bits == 0x7FF0000000000000); // true
|
||||||
|
|
||||||
|
p : Uf32 = ---;
|
||||||
|
p.f = f32.true_min;
|
||||||
|
print("f32.true_min {}\n", p.bits == 0x00000001); // true
|
||||||
|
p.f = f32.max;
|
||||||
|
print("f32.max {}\n", p.bits == 0x7F7FFFFF); // true
|
||||||
|
p.f = f32.min;
|
||||||
|
print("f32.min {}\n", p.bits == 0xFF7FFFFF); // true
|
||||||
|
p.f = f32.epsilon;
|
||||||
|
print("f32.epsilon {}\n", p.bits == 0x34000000); // true
|
||||||
|
p.f = f32.min_positive;
|
||||||
|
print("f32.min_positive {}\n", p.bits == 0x00800000); // true
|
||||||
|
p.f = f32.inf;
|
||||||
|
print("f32.inf {}\n", p.bits == 0x7F800000); // true
|
||||||
|
|
||||||
|
// Defining-property checks — true epsilon is the ULP of 1.0: adding it to 1.0
|
||||||
|
// changes the value, adding half of it does not (round-to-nearest-even).
|
||||||
|
print("(1+eps)!=1 {}\n", (1.0 + f64.epsilon) != 1.0); // true
|
||||||
|
print("(1+eps/2)==1 {}\n", (1.0 + f64.epsilon/2.0) == 1.0); // true
|
||||||
|
print("inf>max {}\n", f64.inf > f64.max); // true
|
||||||
|
// f64.min = -max (the 0xFFEF… bit pattern overflows the i64 literal parser).
|
||||||
|
print("min==-max {}\n", f64.min == -f64.max); // true
|
||||||
|
print("true_min<min_pos {}\n", f64.true_min < f64.min_positive); // true
|
||||||
|
print("true_min>0 {}\n", f64.true_min > 0.0); // true
|
||||||
|
// Quiet NaN: unequal to everything, itself included (mantissa bits not pinned).
|
||||||
|
print("nan!=nan {}\n", f64.nan != f64.nan); // true
|
||||||
|
|
||||||
|
// Result carries the QUERIED type: each binding is declared with the float
|
||||||
|
// type and round-trips, so a mistyped fold (boxed as Any / wrong width) would
|
||||||
|
// not type-check here.
|
||||||
|
e64 : f64 = f64.epsilon;
|
||||||
|
e32 : f32 = f32.epsilon;
|
||||||
|
q : Uf64 = ---; q.f = e64;
|
||||||
|
r : Uf32 = ---; r.f = e32;
|
||||||
|
print("typed eps bits {}\n", q.bits == 0x3CB0000000000000 and r.bits == 0x34000000); // true
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
27
examples/0160-types-float-numeric-limits-errors.sx
Normal file
27
examples/0160-types-float-numeric-limits-errors.sx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Cross-type rules for the numeric-limit accessors. `.min` / `.max` are valid on
|
||||||
|
// BOTH integer and float types, but `.epsilon` / `.min_positive` / `.true_min` /
|
||||||
|
// `.inf` / `.nan` are FLOAT-ONLY. Applying a float-only accessor to an INTEGER
|
||||||
|
// type, or ANY accessor to a non-numeric type, is a clean compile error — never
|
||||||
|
// a silent value, never the `.unresolved` sentinel reaching codegen.
|
||||||
|
//
|
||||||
|
// - float-only accessor on an integer (`s32.epsilon`, `u8.inf`,
|
||||||
|
// `s64.true_min`) → a dedicated "applies only to float types" diagnostic
|
||||||
|
// from the accessor intercept, located at the access;
|
||||||
|
// - any accessor on a non-numeric builtin (`bool.nan`, `string.max`) → the
|
||||||
|
// "numeric limits apply only to integer and float types" diagnostic;
|
||||||
|
// - a user struct (`MyStruct.epsilon`) → the type name is not a builtin, so the
|
||||||
|
// intercept stays out and the existing field-not-found path reports it.
|
||||||
|
// Each case is accurate and located at the access; the program exits non-zero.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
MyStruct :: struct { a: s64; }
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
a := s32.epsilon;
|
||||||
|
b := u8.inf;
|
||||||
|
c := s64.true_min;
|
||||||
|
d := bool.nan;
|
||||||
|
e := string.max;
|
||||||
|
f := MyStruct.epsilon;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
76
examples/0161-types-numeric-limit-value-shadow.sx
Normal file
76
examples/0161-types-numeric-limit-value-shadow.sx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// 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: the raw receiver reads the value, the
|
||||||
|
// bare receiver folds the limit.
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
// 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("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.
|
||||||
|
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 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
examples/expected/0159-types-float-numeric-limits.exit
Normal file
1
examples/expected/0159-types-float-numeric-limits.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
examples/expected/0159-types-float-numeric-limits.stderr
Normal file
1
examples/expected/0159-types-float-numeric-limits.stderr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
20
examples/expected/0159-types-float-numeric-limits.stdout
Normal file
20
examples/expected/0159-types-float-numeric-limits.stdout
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
f64.true_min true
|
||||||
|
f64.max true
|
||||||
|
f64.min true
|
||||||
|
f64.epsilon true
|
||||||
|
f64.min_positive true
|
||||||
|
f64.inf true
|
||||||
|
f32.true_min true
|
||||||
|
f32.max true
|
||||||
|
f32.min true
|
||||||
|
f32.epsilon true
|
||||||
|
f32.min_positive true
|
||||||
|
f32.inf true
|
||||||
|
(1+eps)!=1 true
|
||||||
|
(1+eps/2)==1 true
|
||||||
|
inf>max true
|
||||||
|
min==-max true
|
||||||
|
true_min<min_pos true
|
||||||
|
true_min>0 true
|
||||||
|
nan!=nan true
|
||||||
|
typed eps bits true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
error: type 's32' has no '.epsilon' — '.epsilon' applies only to float types (f32/f64); integer types expose only '.min'/'.max'
|
||||||
|
--> examples/0160-types-float-numeric-limits-errors.sx:20:10
|
||||||
|
|
|
||||||
|
20 | a := s32.epsilon;
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
|
||||||
|
error: type 'u8' has no '.inf' — '.inf' applies only to float types (f32/f64); integer types expose only '.min'/'.max'
|
||||||
|
--> examples/0160-types-float-numeric-limits-errors.sx:21:10
|
||||||
|
|
|
||||||
|
21 | b := u8.inf;
|
||||||
|
| ^^^^^^
|
||||||
|
|
||||||
|
error: type 's64' has no '.true_min' — '.true_min' applies only to float types (f32/f64); integer types expose only '.min'/'.max'
|
||||||
|
--> examples/0160-types-float-numeric-limits-errors.sx:22:10
|
||||||
|
|
|
||||||
|
22 | c := s64.true_min;
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
|
||||||
|
error: type 'bool' has no '.nan' — numeric limits apply only to integer and float types
|
||||||
|
--> examples/0160-types-float-numeric-limits-errors.sx:23:10
|
||||||
|
|
|
||||||
|
23 | d := bool.nan;
|
||||||
|
| ^^^^^^^^
|
||||||
|
|
||||||
|
error: type 'string' has no '.max' — numeric limits apply only to integer and float types
|
||||||
|
--> examples/0160-types-float-numeric-limits-errors.sx:24:10
|
||||||
|
|
|
||||||
|
24 | e := string.max;
|
||||||
|
| ^^^^^^^^^^
|
||||||
|
|
||||||
|
error: field 'epsilon' not found on type 'Any'
|
||||||
|
--> examples/0160-types-float-numeric-limits-errors.sx:25:10
|
||||||
|
|
|
||||||
|
25 | f := MyStruct.epsilon;
|
||||||
|
| ^^^^^^^^^^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
77
issues/0092-raw-reserved-value-numeric-limit-shadow.md
Normal file
77
issues/0092-raw-reserved-value-numeric-limit-shadow.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
> **RESOLVED** (NL.2 attempt 3). Root cause: the numeric-limit accessor
|
||||||
|
> intercept treated ANY receiver whose text matched a builtin numeric type
|
||||||
|
> name as a TYPE receiver, without first checking whether that identifier
|
||||||
|
> resolved to 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.
|
||||||
|
>
|
||||||
|
> Fix (value-binding precedence for `.identifier` receivers; `.type_expr`
|
||||||
|
> receivers are unambiguous types and never shadowed):
|
||||||
|
> - `src/ir/lower.zig` — `lowerNumericLimit`: after confirming the receiver is
|
||||||
|
> a builtin numeric type name and the field is a limit accessor, return null
|
||||||
|
> (defer to ordinary field lowering) when `fa.object` is an `.identifier`
|
||||||
|
> that `Scope.lookup` resolves to a value binding.
|
||||||
|
> - `src/ir/expr_typer.zig` — numeric-limit inference arm: mirror the same
|
||||||
|
> guard so inferred types match lowering (avoids the issue-0083 two-resolver
|
||||||
|
> desync).
|
||||||
|
>
|
||||||
|
> Bare `f64.epsilon` / `s32.max` (no shadowing binding) still fold — the parser
|
||||||
|
> classifies a bare builtin name as a `.type_expr` (parser.zig:2743), so the
|
||||||
|
> bare receiver is never value-shadowed even in a scope where `` `f64 `` is
|
||||||
|
> bound. Float-only-on-int and non-numeric-receiver errors are unchanged.
|
||||||
|
>
|
||||||
|
> Regression: `examples/0161-types-numeric-limit-value-shadow.sx` (raw
|
||||||
|
> `` `f64 ``/`` `s32 ``/`` `u8 `` value reads coexisting with bare folds) +
|
||||||
|
> unit test in `src/ir/expr_typer.test.zig`. NL.1 (`examples/0148`) / NL.2
|
||||||
|
> (`examples/0159`, `examples/0160`) unregressed.
|
||||||
|
|
||||||
|
# 0092 — numeric-limit intercept hijacks raw reserved-spelled value receivers
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Field access on a raw reserved-spelled value binding is interpreted as a builtin
|
||||||
|
type numeric-limit access instead of an ordinary value field access. Observed:
|
||||||
|
the repro prints `0.000000 2147483647` (`f64.epsilon` / `s32.max`). Expected:
|
||||||
|
it prints `12 78` from the `Box` fields.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Box :: struct { epsilon: s64; max: s64; }
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
`f64 := Box.{ epsilon = 12, max = 34 };
|
||||||
|
`s32 := Box.{ epsilon = 56, max = 78 };
|
||||||
|
print("{} {}\n", `f64.epsilon, `s32.max);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
Investigate issue 0092: raw reserved-spelled value receivers are being captured
|
||||||
|
by the numeric-limit accessor intercept. In `src/ir/lower.zig`, start at
|
||||||
|
`Lowering.lowerFieldAccess` and `Lowering.lowerNumericLimit` (currently around
|
||||||
|
`lower.zig:4826` / `lower.zig:4923`). The intercept treats any
|
||||||
|
`.identifier`/`.type_expr` receiver whose text is a builtin type name as a type
|
||||||
|
receiver, without first checking whether the identifier resolves to a value
|
||||||
|
binding in the current lexical scope. Mirror the fix in `src/ir/expr_typer.zig`
|
||||||
|
around the numeric-limit inference arm so inferred types match lowering.
|
||||||
|
|
||||||
|
Likely fix: for `.identifier` receivers, prefer an in-scope value binding
|
||||||
|
(`Scope.lookup`) over the builtin-type numeric-limit intercept; keep the
|
||||||
|
intercept for actual type receivers (`.type_expr`, and bare reserved integer
|
||||||
|
names with no value binding). If raw provenance is available in the AST, using it
|
||||||
|
to disambiguate is also acceptable, but the observable rule must be that
|
||||||
|
`` `f64.epsilon `` reads the value field when `` `f64 `` is a value binding,
|
||||||
|
while bare `f64.epsilon` / `s32.max` still fold as numeric limits.
|
||||||
|
|
||||||
|
Verification: pin a regression test from the repro above. It should print
|
||||||
|
`12 78`. Also verify the existing numeric-limit examples still pass:
|
||||||
|
`examples/0148-types-int-numeric-limits.sx`,
|
||||||
|
`examples/0159-types-float-numeric-limits.sx`, and the negative diagnostics in
|
||||||
|
`examples/0160-types-float-numeric-limits-errors.sx`.
|
||||||
84
issues/0093-global-raw-value-numeric-limit-shadow.md
Normal file
84
issues/0093-global-raw-value-numeric-limit-shadow.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
> **RESOLVED** (NL.2 attempt 4). Root cause: the issue-0092 fix guarded the
|
||||||
|
> numeric-limit intercept against value shadowing using ONLY lexical
|
||||||
|
> `Scope.lookup`. But the ordinary identifier field-access path resolves a
|
||||||
|
> value through THREE sources (`expr_typer.zig` `.identifier` arm): lexical
|
||||||
|
> `scope` → program `global_names` → module value constants
|
||||||
|
> `module_const_map`. A backtick raw identifier bound at MODULE scope
|
||||||
|
> (`` `f64 := Box.{…} ``, a global, or `` `f64 :: Box.{…} ``, a module const)
|
||||||
|
> is registered in `global_names` / `module_const_map`, NOT in `Scope`, so the
|
||||||
|
> scope-only guard missed it and the intercept still folded `` `f64.epsilon ``
|
||||||
|
> to the numeric limit — the same silent-wrong-value bug as 0092, one source
|
||||||
|
> deeper. The module-const variant has the same root cause and is covered by
|
||||||
|
> the same fix (no separate issue).
|
||||||
|
>
|
||||||
|
> Fix (close ALL THREE value-binding sources in one pass): a single shared
|
||||||
|
> helper `Lowering.identifierBindsValue(name)` returns true when `name`
|
||||||
|
> resolves through `scope.lookup` OR `program_index.global_names` OR
|
||||||
|
> `program_index.module_const_map`. Used in BOTH resolvers so they cannot
|
||||||
|
> desync (issue-0083 two-resolver class):
|
||||||
|
> - `src/ir/lower.zig` — `lowerNumericLimit`: defer to ordinary field lowering
|
||||||
|
> (return null) when an `.identifier` receiver `identifierBindsValue`.
|
||||||
|
> - `src/ir/expr_typer.zig` — numeric-limit inference arm: the `shadowed`
|
||||||
|
> check now calls the same helper.
|
||||||
|
>
|
||||||
|
> A bare `f64.epsilon` / `s32.max` (a `.type_expr` receiver, never an
|
||||||
|
> `.identifier`) still folds, even when a global or module-const raw value of
|
||||||
|
> the same spelling exists — the bare receiver is never value-shadowed.
|
||||||
|
> Float-only-on-int and non-numeric-receiver errors are unchanged.
|
||||||
|
>
|
||||||
|
> Regression: `examples/0161-types-numeric-limit-value-shadow.sx` now exercises
|
||||||
|
> 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. Unit test `src/ir/expr_typer.test.zig` pins the global
|
||||||
|
> + module-const sources. NL.1 (`examples/0148`) / NL.2 (`examples/0159`,
|
||||||
|
> `examples/0160`) unregressed.
|
||||||
|
|
||||||
|
# 0093 — numeric-limit intercept hijacks global raw reserved-spelled value receivers
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Field access on a **global** raw reserved-spelled value binding is interpreted as
|
||||||
|
a builtin type numeric-limit access instead of an ordinary value field access.
|
||||||
|
Observed: the repro prints `0.000000 2147483647` (`f64.epsilon` /
|
||||||
|
`s32.max`). Expected: it prints `12 78` from the `Box` fields.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Box :: struct { epsilon: s64; max: s64; }
|
||||||
|
|
||||||
|
`f64 := Box.{ epsilon = 12, max = 34 };
|
||||||
|
`s32 := Box.{ epsilon = 56, max = 78 };
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("{} {}\n", `f64.epsilon, `s32.max);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
Investigate issue 0093: the issue-0092 value-binding precedence fix covers
|
||||||
|
lexical locals but misses global raw value bindings. In `src/ir/lower.zig`, start
|
||||||
|
at `Lowering.lowerNumericLimit` and the new issue-0092 guard around
|
||||||
|
`Scope.lookup`. That guard returns `null` for a shadowing local, but global raw
|
||||||
|
bindings are registered in `ProgramIndex.global_names` (and module constants in
|
||||||
|
`ProgramIndex.module_const_map`), not in `Scope`, so an `.identifier` receiver
|
||||||
|
whose text is `f64` / `s32` still folds to a numeric limit before ordinary
|
||||||
|
global field lowering can read the value. Mirror the same rule in
|
||||||
|
`src/ir/expr_typer.zig` so inferred types match lowering.
|
||||||
|
|
||||||
|
Likely fix: for `.identifier` numeric-limit receivers, prefer any in-scope value
|
||||||
|
binding source over the builtin-type fold: lexical `Scope.lookup`, global values
|
||||||
|
(`program_index.global_names`), and module value constants where applicable.
|
||||||
|
Keep `.type_expr` receivers folding as type receivers, so bare `f64.epsilon` and
|
||||||
|
`s32.max` still fold even when a raw global value of the same spelling exists.
|
||||||
|
|
||||||
|
Verification: pin the repro above as a regression. It should print `12 78`.
|
||||||
|
Also verify the existing numeric-limit examples still pass:
|
||||||
|
`examples/0148-types-int-numeric-limits.sx`,
|
||||||
|
`examples/0159-types-float-numeric-limits.sx`,
|
||||||
|
`examples/0160-types-float-numeric-limits-errors.sx`, and
|
||||||
|
`examples/0161-types-numeric-limit-value-shadow.sx`.
|
||||||
11
readme.md
11
readme.md
@@ -90,7 +90,16 @@ Options:
|
|||||||
a compile-time constant of that type: `s64.max` → `9223372036854775807`,
|
a compile-time constant of that type: `s64.max` → `9223372036854775807`,
|
||||||
`u8.min` → `0`, `s3.max` → `3`. It works for every width `s1`..`s64` / `u1`..`u64`
|
`u8.min` → `0`, `s3.max` → `3`. It works for every width `s1`..`s64` / `u1`..`u64`
|
||||||
plus `usize`/`isize`, and is usable anywhere a constant of that type is — including
|
plus `usize`/`isize`, and is usable anywhere a constant of that type is — including
|
||||||
array dimensions (`[u8.max]T` is a 255-element array).
|
array dimensions (`[u8.max]T` is a 255-element array). The float types `f32`/`f64`
|
||||||
|
expose `.min` / `.max` too (with `.min` = most-negative finite = `-max`, **not**
|
||||||
|
C's `DBL_MIN`) plus the float-only `.epsilon` (ULP of 1.0, not C#'s denormal
|
||||||
|
`Epsilon`), `.min_positive` (smallest normal = C `DBL_MIN`), `.true_min` (smallest
|
||||||
|
subnormal — beware flush-to-zero CPU modes), `.inf`, and `.nan`. A float-only
|
||||||
|
accessor on an integer (`s32.epsilon`), or any accessor on a non-numeric type, is
|
||||||
|
a clean compile error. The fold applies only to a bare type-name receiver: a raw
|
||||||
|
identifier that binds a value shadowing a type name (`` `f64 := … `` then
|
||||||
|
`` `f64.epsilon ``) reads the value's field, not the limit — for a local, global,
|
||||||
|
or module-constant binding alike. See `specs.md` → Numeric Limits.
|
||||||
|
|
||||||
### Declarations
|
### Declarations
|
||||||
|
|
||||||
|
|||||||
68
specs.md
68
specs.md
@@ -283,6 +283,74 @@ n := u64.max; // 18446744073709551615 (all-ones)
|
|||||||
`string`, a pointer, a `struct`, `void`, an `enum`) is a compile error, never
|
`string`, a pointer, a `struct`, `void`, an `enum`) is a compile error, never
|
||||||
a silent value.
|
a silent value.
|
||||||
|
|
||||||
|
The **float** types `f32` and `f64` expose the same `.min` / `.max` plus a set of
|
||||||
|
float-only accessors. Each folds, at compile time, to a constant of the queried
|
||||||
|
float type (the same `lowerNumericLimit` intercept, via `builder.constFloat`):
|
||||||
|
|
||||||
|
```sx
|
||||||
|
hi := f64.max; // largest finite double
|
||||||
|
lo := f64.min; // most-NEGATIVE finite = -max (NOT C's DBL_MIN)
|
||||||
|
eps := f64.epsilon; // ULP of 1.0 (f64 = 2^-52, f32 = 2^-23)
|
||||||
|
mp := f64.min_positive; // smallest positive NORMAL (= C DBL_MIN / Rust MIN_POSITIVE)
|
||||||
|
tm := f64.true_min; // smallest positive SUBNORMAL (next value above 0.0)
|
||||||
|
pin := f64.inf; // +infinity
|
||||||
|
qn := f64.nan; // a quiet NaN
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Receiver.** `f32` or `f64`.
|
||||||
|
- **Shared with integers.** `.min` / `.max` are valid on BOTH integer and float
|
||||||
|
types. `.min` is the most-NEGATIVE finite value, i.e. `-max` — consistent with
|
||||||
|
the integer `.min`, and deliberately **NOT** C's `DBL_MIN`/`FLT_MIN` (which is
|
||||||
|
the smallest positive normal; that is `.min_positive` here).
|
||||||
|
- **Float-only accessors.**
|
||||||
|
- `.epsilon` — the ULP of `1.0`: the gap between `1.0` and the next
|
||||||
|
representable value (`f64 = 2^-52 ≈ 2.22e-16`, `f32 = 2^-23`). This is the
|
||||||
|
**machine epsilon** used for relative-tolerance comparisons, **NOT** C#'s
|
||||||
|
`Double.Epsilon` (which is the smallest denormal — that is `.true_min` here).
|
||||||
|
Defining property: `1.0 + epsilon != 1.0` while `1.0 + epsilon/2.0 == 1.0`.
|
||||||
|
- `.min_positive` — the smallest positive **NORMAL** value (`f64 = 2^-1022`,
|
||||||
|
`f32 = 2^-126`). Equals C's `DBL_MIN` / Rust's `MIN_POSITIVE`.
|
||||||
|
- `.true_min` — the smallest positive **SUBNORMAL**: the next value above `0.0`
|
||||||
|
(`f64` bits `0x0000000000000001 = 2^-1074`, `f32` bits `0x00000001 = 2^-149`).
|
||||||
|
Named `true_min` (after Zig's `floatTrueMin`) to avoid the Java/Go/JS
|
||||||
|
`MIN_VALUE` footgun, where a bare `MIN_VALUE` names the smallest *subnormal*
|
||||||
|
yet reads like the most-negative value.
|
||||||
|
- **FTZ/DAZ caveat.** Subnormals are exactly the values that vanish under
|
||||||
|
flush-to-zero (FTZ) / denormals-are-zero (DAZ) CPU modes. If such a mode is
|
||||||
|
active, a loaded `.true_min` can flush to `0.0` on the **first arithmetic
|
||||||
|
operation** that touches it. The folded constant always carries the exact
|
||||||
|
subnormal bit pattern; read or store it through a bit reinterpret *before*
|
||||||
|
any arithmetic if you need the true value to survive. Numerical-library
|
||||||
|
authors who toggle FTZ/DAZ should not be surprised when `true_min * 1.0`
|
||||||
|
reads back as `0.0`.
|
||||||
|
- `.inf` — positive infinity (`inf > max`).
|
||||||
|
- `.nan` — a quiet NaN. The exact mantissa bits are not pinned; the only
|
||||||
|
guaranteed property is that it is unequal to everything, itself included
|
||||||
|
(`nan != nan` is `true` — native float `!=` lowers unordered, issue 0091).
|
||||||
|
- **Float-only on an integer is an error.** `.epsilon` / `.min_positive` /
|
||||||
|
`.true_min` / `.inf` / `.nan` applied to an integer type (`s32.epsilon`,
|
||||||
|
`u8.inf`, `s64.true_min`) is a clean compile error — integer types expose only
|
||||||
|
`.min` / `.max`.
|
||||||
|
- **Pinning the values.** The lexer has no exponent notation and the default
|
||||||
|
float formatter is crude (issue 0090), so float limits can be asserted neither
|
||||||
|
by literal comparison nor by printing. Reinterpret the bits through an untagged
|
||||||
|
union (`union { f: f64; bits: u64 }`) and compare against the exact IEEE-754
|
||||||
|
pattern — `f64.max = 0x7FEFFFFFFFFFFFFF`, `min = 0xFFEFFFFFFFFFFFFF`,
|
||||||
|
`epsilon = 0x3CB0000000000000`, `min_positive = 0x0010000000000000`,
|
||||||
|
`true_min = 0x0000000000000001`, `inf = 0x7FF0000000000000`; the `f32` set is
|
||||||
|
`0x7F7FFFFF` / `0xFF7FFFFF` / `0x34000000` / `0x00800000` / `0x00000001` /
|
||||||
|
`0x7F800000`.
|
||||||
|
- **Type receiver vs. a shadowing value binding.** A numeric-limit access folds
|
||||||
|
only when the receiver is a builtin numeric **type name** (`f64.epsilon`,
|
||||||
|
`s32.max`, `u8.max`). A backtick raw identifier that binds a *value* whose
|
||||||
|
spelling shadows a type name (F0.6) is an ordinary value: `` `f64.epsilon ``
|
||||||
|
reads that value's `epsilon` field — it does **not** fold to the limit. This
|
||||||
|
holds for **every** value-binding kind — a `` `f64 := … `` local, a module-scope
|
||||||
|
global, or a `` `f64 :: … `` module constant — so the fold can never silently
|
||||||
|
hijack a raw value, whatever its scope. The two never collide: a bare builtin
|
||||||
|
name in expression position is always a type, and only the raw `` `…` `` spelling
|
||||||
|
can bind a value under it.
|
||||||
|
|
||||||
### Enum Types
|
### Enum Types
|
||||||
User-defined sum types with named variants. Variants may optionally carry typed data (tagged unions). Internally, payload-less enums are represented as `i64` (variant index). Enums with payloads are represented as `{ i64, [max_payload_size x i8] }` (tag + data).
|
User-defined sum types with named variants. Variants may optionally carry typed data (tagged unions). Internally, payload-less enums are represented as `i64` (variant index). Enums with payloads are represented as `{ i64, [max_payload_size x i8] }` (tag + data).
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const Node = ast.Node;
|
|||||||
|
|
||||||
const ir_mod = @import("ir.zig");
|
const ir_mod = @import("ir.zig");
|
||||||
const TypeId = ir_mod.TypeId;
|
const TypeId = ir_mod.TypeId;
|
||||||
|
const Ref = ir_mod.Ref;
|
||||||
const Lowering = ir_mod.Lowering;
|
const Lowering = ir_mod.Lowering;
|
||||||
|
const Scope = @import("lower.zig").Scope;
|
||||||
|
|
||||||
fn node(data: ast.Node.Data) Node {
|
fn node(data: ast.Node.Data) Node {
|
||||||
return .{ .span = .{ .start = 0, .end = 0 }, .data = data };
|
return .{ .span = .{ .start = 0, .end = 0 }, .data = data };
|
||||||
@@ -73,3 +75,105 @@ test "expr_typer: deref of a non-pointer is unresolved" {
|
|||||||
var deref_n = node(.{ .deref_expr = .{ .operand = &i } });
|
var deref_n = node(.{ .deref_expr = .{ .operand = &i } });
|
||||||
try std.testing.expectEqual(TypeId.unresolved, l.inferExprType(&deref_n));
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,10 +126,13 @@ pub const ExprTyper = struct {
|
|||||||
if (info.ty) |t| return t;
|
if (info.ty) |t| return t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Numeric-limit accessor: `<IntType>.min` / `.max` is a comptime
|
// Numeric-limit accessor: `<Type>.min`/`.max` (int or float) or a
|
||||||
// const of the queried integer type — mirrors the lowerFieldAccess
|
// float-only `.epsilon`/`.min_positive`/`.true_min`/`.inf`/`.nan`
|
||||||
// intercept so inference reports the same type (without it the
|
// is a comptime const of the queried type — mirrors the
|
||||||
// const would be mistyped, e.g. boxed into an Any slot).
|
// lowerFieldAccess intercept so inference reports the same type
|
||||||
|
// (without it the const would be mistyped, e.g. boxed into an Any
|
||||||
|
// slot). Only valid folds carry a type here; the cross-type error
|
||||||
|
// cases fall through (lowerNumericLimit emits the diagnostic).
|
||||||
{
|
{
|
||||||
const type_name: ?[]const u8 = switch (fa.object.data) {
|
const type_name: ?[]const u8 = switch (fa.object.data) {
|
||||||
.identifier => |id| id.name,
|
.identifier => |id| id.name,
|
||||||
@@ -137,7 +140,18 @@ pub const ExprTyper = struct {
|
|||||||
else => null,
|
else => null,
|
||||||
};
|
};
|
||||||
if (type_name) |tn| {
|
if (type_name) |tn| {
|
||||||
if (TypeResolver.integerLimitFor(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
|
||||||
|
// (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
|
||||||
|
self.l.identifierBindsValue(tn);
|
||||||
|
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;
|
if (TypeResolver.resolveBuiltinName(tn, &self.l.module.types)) |t| return t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4904,33 +4904,76 @@ pub const Lowering = struct {
|
|||||||
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
|
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Numeric-limit accessor intercept (`<IntType>.min` / `.max`), a sibling of
|
/// True when an `.identifier` receiver text resolves to an in-scope VALUE
|
||||||
/// the `error.X` / `Struct.CONST` / pack-arity identifier-receiver intercepts
|
/// binding rather than a builtin type. A backtick raw identifier (F0.6) can
|
||||||
/// in `lowerFieldAccess`. Folds an integer type's `.min`/`.max` to a comptime
|
/// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``);
|
||||||
/// const of that type via the shared `TypeResolver` width logic (no second
|
/// such a value is reachable through the same three sources the ordinary
|
||||||
/// width parser) + the existing `constInt` const path. Returns null when this
|
/// identifier field-access path consults (see `expr_typer` `.identifier`
|
||||||
/// is not an integer-limit access, so the caller continues normal field
|
/// arm): lexical `scope`, program `global_names`, and module value
|
||||||
/// lowering. A `.min`/`.max` on a builtin NON-numeric receiver
|
/// constants `module_const_map`. The numeric-limit intercept must defer to
|
||||||
/// (`bool`/`string`/`void`/`Any`/`noreturn`) is a clean diagnostic here (then
|
/// ordinary field access whenever ANY of the three binds the name, so a
|
||||||
/// a placeholder, so lowering finishes and `hasErrors()` aborts the build); a
|
/// raw value field read is never hijacked into a numeric-limit fold
|
||||||
/// float receiver falls through (float limits are NL.2).
|
/// (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
|
||||||
|
/// `lowerFieldAccess`. Folds the limit to a comptime const of the queried
|
||||||
|
/// type via the shared `TypeResolver` logic (no second computor) + the
|
||||||
|
/// existing `constInt` / `constFloat` const paths:
|
||||||
|
/// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`);
|
||||||
|
/// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/
|
||||||
|
/// `.nan` → `constFloat` (via `floatLimitFor`).
|
||||||
|
/// Returns null when the field is not a limit accessor, or the receiver is not
|
||||||
|
/// a builtin type (a user struct → ordinary field lowering reports
|
||||||
|
/// field-not-found). Two clean diagnostics (then a placeholder, so lowering
|
||||||
|
/// finishes and `hasErrors()` aborts the build):
|
||||||
|
/// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`);
|
||||||
|
/// - any accessor on a builtin NON-numeric receiver
|
||||||
|
/// (`bool`/`string`/`void`/`Any`/`noreturn`).
|
||||||
fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref {
|
fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref {
|
||||||
const name = switch (fa.object.data) {
|
const name = switch (fa.object.data) {
|
||||||
.identifier => |id| id.name,
|
.identifier => |id| id.name,
|
||||||
.type_expr => |te| te.name,
|
.type_expr => |te| te.name,
|
||||||
else => return null,
|
else => return null,
|
||||||
};
|
};
|
||||||
if (!std.mem.eql(u8, fa.field, "min") and !std.mem.eql(u8, fa.field, "max")) return null;
|
if (!TypeResolver.isLimitField(fa.field)) return null;
|
||||||
const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse 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
|
||||||
|
// 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| {
|
if (TypeResolver.integerLimitFor(name, fa.field)) |value| {
|
||||||
return self.builder.constInt(value, ty);
|
return self.builder.constInt(value, ty);
|
||||||
}
|
}
|
||||||
// A builtin receiver that is not an integer: floats are NL.2 (fall
|
if (TypeResolver.floatLimitFor(name, fa.field)) |value| {
|
||||||
// through), every other builtin (bool/string/void/Any/noreturn) has no
|
return self.builder.constFloat(value, ty);
|
||||||
// numeric limit.
|
}
|
||||||
if (ty == .f32 or ty == .f64) return null;
|
// The field is a limit accessor, but it does not apply to this type.
|
||||||
if (self.diagnostics) |d| {
|
if (self.diagnostics) |d| {
|
||||||
d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field });
|
if (TypeResolver.integerWidthSign(name) != null) {
|
||||||
|
// Integer receiver + a float-only accessor.
|
||||||
|
d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field });
|
||||||
|
} else {
|
||||||
|
// Non-numeric builtin receiver (bool/string/void/Any/noreturn).
|
||||||
|
d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return self.emitPlaceholder(fa.field);
|
return self.emitPlaceholder(fa.field);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,3 +246,74 @@ test "TypeResolver.integerLimitFor: null for non-integer receivers and non-limit
|
|||||||
try std.testing.expect(TypeResolver.integerLimitFor("s64", "len") == null);
|
try std.testing.expect(TypeResolver.integerLimitFor("s64", "len") == null);
|
||||||
try std.testing.expect(TypeResolver.integerLimitFor("u8", "epsilon") == null);
|
try std.testing.expect(TypeResolver.integerLimitFor("u8", "epsilon") == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "TypeResolver.isLimitField: the accessor set, nothing else" {
|
||||||
|
// The full numeric-limit surface — int .min/.max plus the float-only ones.
|
||||||
|
for ([_][]const u8{ "min", "max", "epsilon", "min_positive", "true_min", "inf", "nan" }) |f| {
|
||||||
|
try std.testing.expect(TypeResolver.isLimitField(f));
|
||||||
|
}
|
||||||
|
// Ordinary fields / near-misses are not limit accessors.
|
||||||
|
try std.testing.expect(!TypeResolver.isLimitField("len"));
|
||||||
|
try std.testing.expect(!TypeResolver.isLimitField("ptr"));
|
||||||
|
try std.testing.expect(!TypeResolver.isLimitField("maximum"));
|
||||||
|
try std.testing.expect(!TypeResolver.isLimitField("Min"));
|
||||||
|
try std.testing.expect(!TypeResolver.isLimitField(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "TypeResolver.floatLimitFor: pinned f64 bit patterns" {
|
||||||
|
const L = struct {
|
||||||
|
fn bits(name: []const u8, field: []const u8) u64 {
|
||||||
|
return @bitCast(TypeResolver.floatLimitFor(name, field).?);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Exact IEEE-754 double bit patterns (the same values the example pins via
|
||||||
|
// a runtime bit reinterpret).
|
||||||
|
try std.testing.expectEqual(@as(u64, 0x7FEFFFFFFFFFFFFF), L.bits("f64", "max"));
|
||||||
|
try std.testing.expectEqual(@as(u64, 0xFFEFFFFFFFFFFFFF), L.bits("f64", "min")); // -max
|
||||||
|
try std.testing.expectEqual(@as(u64, 0x3CB0000000000000), L.bits("f64", "epsilon"));
|
||||||
|
try std.testing.expectEqual(@as(u64, 0x0010000000000000), L.bits("f64", "min_positive"));
|
||||||
|
try std.testing.expectEqual(@as(u64, 0x0000000000000001), L.bits("f64", "true_min"));
|
||||||
|
try std.testing.expectEqual(@as(u64, 0x7FF0000000000000), L.bits("f64", "inf"));
|
||||||
|
// .min = -max (NOT C's DBL_MIN, which is min_positive); the ordering holds.
|
||||||
|
const v = struct {
|
||||||
|
fn f(name: []const u8, field: []const u8) f64 {
|
||||||
|
return TypeResolver.floatLimitFor(name, field).?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try std.testing.expectEqual(v.f("f64", "min"), -v.f("f64", "max"));
|
||||||
|
try std.testing.expect(v.f("f64", "true_min") < v.f("f64", "min_positive"));
|
||||||
|
try std.testing.expect(v.f("f64", "true_min") > 0.0);
|
||||||
|
try std.testing.expect(std.math.isInf(v.f("f64", "inf")));
|
||||||
|
// Quiet NaN: unequal to itself; exact mantissa bits intentionally not pinned.
|
||||||
|
try std.testing.expect(std.math.isNan(v.f("f64", "nan")));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "TypeResolver.floatLimitFor: pinned f32 bit patterns (widened value narrows losslessly)" {
|
||||||
|
const L = struct {
|
||||||
|
// floatLimitFor widens every f32 limit to f64; narrowing back is lossless
|
||||||
|
// (the codegen path does the same via constFloat at the queried width).
|
||||||
|
fn bits(name: []const u8, field: []const u8) u32 {
|
||||||
|
return @bitCast(@as(f32, @floatCast(TypeResolver.floatLimitFor(name, field).?)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x7F7FFFFF), L.bits("f32", "max"));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0xFF7FFFFF), L.bits("f32", "min")); // -max
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x34000000), L.bits("f32", "epsilon"));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x00800000), L.bits("f32", "min_positive"));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x00000001), L.bits("f32", "true_min"));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x7F800000), L.bits("f32", "inf"));
|
||||||
|
const nan_v: f32 = @floatCast(TypeResolver.floatLimitFor("f32", "nan").?);
|
||||||
|
try std.testing.expect(std.math.isNan(nan_v));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "TypeResolver.floatLimitFor: null for non-float receivers and non-limit fields" {
|
||||||
|
// Integer / non-numeric / user names are not float-limit folds.
|
||||||
|
try std.testing.expect(TypeResolver.floatLimitFor("s32", "epsilon") == null);
|
||||||
|
try std.testing.expect(TypeResolver.floatLimitFor("u64", "max") == null);
|
||||||
|
try std.testing.expect(TypeResolver.floatLimitFor("usize", "min") == null);
|
||||||
|
try std.testing.expect(TypeResolver.floatLimitFor("bool", "nan") == null);
|
||||||
|
try std.testing.expect(TypeResolver.floatLimitFor("MyStruct", "inf") == null);
|
||||||
|
// A builtin float with a non-limit field is not a fold here.
|
||||||
|
try std.testing.expect(TypeResolver.floatLimitFor("f64", "len") == null);
|
||||||
|
try std.testing.expect(TypeResolver.floatLimitFor("f32", "ptr") == null);
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,6 +137,49 @@ pub const TypeResolver = struct {
|
|||||||
return integerLimitBits(wi, want_max);
|
return integerLimitBits(wi, want_max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The full numeric-limit accessor field set: `.min`/`.max` (valid on int AND
|
||||||
|
/// float) plus the float-only `.epsilon`/`.min_positive`/`.true_min`/`.inf`/
|
||||||
|
/// `.nan`. THE single trigger for the `lowerNumericLimit` intercept — only a
|
||||||
|
/// field in this set is treated as a limit access; anything else falls through
|
||||||
|
/// to ordinary field lowering. Keeps the accessor name set in one place so the
|
||||||
|
/// intercept and `expr_typer` can't recognize different surfaces.
|
||||||
|
pub fn isLimitField(field: []const u8) bool {
|
||||||
|
const names = [_][]const u8{ "min", "max", "epsilon", "min_positive", "true_min", "inf", "nan" };
|
||||||
|
for (names) |n| if (std.mem.eql(u8, field, n)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `<FloatType>.<field>` → the limit as an `f64` value (the queried type is
|
||||||
|
/// `f32`/`f64`; every f32 limit is exactly representable in f64, so widening
|
||||||
|
/// is lossless and the caller pairs the value with the queried `TypeId` —
|
||||||
|
/// `builder.constFloat` narrows it back at emit), or null when `name` is not a
|
||||||
|
/// builtin float type or `field` is not a limit accessor. Values come straight
|
||||||
|
/// from `std.math` (`floatMax`/`floatEps`/`floatMin`/`floatTrueMin`/`inf`/`nan`):
|
||||||
|
/// - `.min` = most-NEGATIVE finite (`-max`, NOT C's DBL_MIN)
|
||||||
|
/// - `.max` = largest finite
|
||||||
|
/// - `.epsilon` = ULP of 1.0 (`floatEps`; f64 = 2^-52, f32 = 2^-23)
|
||||||
|
/// - `.min_positive` = smallest positive NORMAL (`floatMin`; = C DBL_MIN)
|
||||||
|
/// - `.true_min` = smallest positive SUBNORMAL (`floatTrueMin`)
|
||||||
|
/// - `.inf` = +infinity, `.nan` = a quiet NaN
|
||||||
|
/// THE single name+field → float fold, shared by the value path (lower.zig)
|
||||||
|
/// and `expr_typer` so they can't disagree.
|
||||||
|
pub fn floatLimitFor(name: []const u8, field: []const u8) ?f64 {
|
||||||
|
if (std.mem.eql(u8, name, "f64")) return floatLimitValue(f64, field);
|
||||||
|
if (std.mem.eql(u8, name, "f32")) return floatLimitValue(f32, field);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn floatLimitValue(comptime T: type, field: []const u8) ?f64 {
|
||||||
|
if (std.mem.eql(u8, field, "min")) return -@as(f64, std.math.floatMax(T));
|
||||||
|
if (std.mem.eql(u8, field, "max")) return @as(f64, std.math.floatMax(T));
|
||||||
|
if (std.mem.eql(u8, field, "epsilon")) return @as(f64, std.math.floatEps(T));
|
||||||
|
if (std.mem.eql(u8, field, "min_positive")) return @as(f64, std.math.floatMin(T));
|
||||||
|
if (std.mem.eql(u8, field, "true_min")) return @as(f64, std.math.floatTrueMin(T));
|
||||||
|
if (std.mem.eql(u8, field, "inf")) return @as(f64, std.math.inf(T));
|
||||||
|
if (std.mem.eql(u8, field, "nan")) return @as(f64, std.math.nan(T));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Single owner of structural AST-type-shape construction. Builds the
|
/// Single owner of structural AST-type-shape construction. Builds the
|
||||||
/// shapes whose `TypeId` is fully determined by their node kind plus their
|
/// shapes whose `TypeId` is fully determined by their node kind plus their
|
||||||
/// element types resolved through `inner.resolveInner`: `*T`, `[*]T`, `[]T`,
|
/// element types resolved through `inner.resolveInner`: `*T`, `[*]T`, `[]T`,
|
||||||
|
|||||||
Reference in New Issue
Block a user