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`,
|
||||
`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
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
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 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,105 @@ 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));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// Numeric-limit accessor: `<IntType>.min` / `.max` is a comptime
|
||||
// const of the queried integer type — mirrors the lowerFieldAccess
|
||||
// intercept so inference reports the same type (without it the
|
||||
// const would be mistyped, e.g. boxed into an Any slot).
|
||||
// Numeric-limit accessor: `<Type>.min`/`.max` (int or float) or a
|
||||
// float-only `.epsilon`/`.min_positive`/`.true_min`/`.inf`/`.nan`
|
||||
// is a comptime const of the queried type — mirrors the
|
||||
// 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) {
|
||||
.identifier => |id| id.name,
|
||||
@@ -137,7 +140,18 @@ pub const ExprTyper = struct {
|
||||
else => null,
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4904,33 +4904,76 @@ pub const Lowering = struct {
|
||||
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
|
||||
}
|
||||
|
||||
/// Numeric-limit accessor intercept (`<IntType>.min` / `.max`), a sibling of
|
||||
/// the `error.X` / `Struct.CONST` / pack-arity identifier-receiver intercepts
|
||||
/// in `lowerFieldAccess`. Folds an integer type's `.min`/`.max` to a comptime
|
||||
/// const of that type via the shared `TypeResolver` width logic (no second
|
||||
/// width parser) + the existing `constInt` const path. Returns null when this
|
||||
/// is not an integer-limit access, so the caller continues normal field
|
||||
/// lowering. A `.min`/`.max` on a builtin NON-numeric receiver
|
||||
/// (`bool`/`string`/`void`/`Any`/`noreturn`) is a clean diagnostic here (then
|
||||
/// a placeholder, so lowering finishes and `hasErrors()` aborts the build); a
|
||||
/// float receiver falls through (float limits are NL.2).
|
||||
/// 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
|
||||
/// `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 {
|
||||
const name = switch (fa.object.data) {
|
||||
.identifier => |id| id.name,
|
||||
.type_expr => |te| te.name,
|
||||
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;
|
||||
|
||||
// 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| {
|
||||
return self.builder.constInt(value, ty);
|
||||
}
|
||||
// A builtin receiver that is not an integer: floats are NL.2 (fall
|
||||
// through), every other builtin (bool/string/void/Any/noreturn) has no
|
||||
// numeric limit.
|
||||
if (ty == .f32 or ty == .f64) return null;
|
||||
if (TypeResolver.floatLimitFor(name, fa.field)) |value| {
|
||||
return self.builder.constFloat(value, ty);
|
||||
}
|
||||
// The field is a limit accessor, but it does not apply to this type.
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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("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);
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// shapes whose `TypeId` is fully determined by their node kind plus their
|
||||
/// element types resolved through `inner.resolveInner`: `*T`, `[*]T`, `[]T`,
|
||||
|
||||
Reference in New Issue
Block a user