fix(ir): complete const-float evaluator — resolve float-const leaves too [F0.11]
Completes issue 0095: a non-integral float→int narrowing via a FLOAT-const leaf (`F : f64 : 2.5; y : s64 = F + 0.25` = 2.75) silently truncated to 2. `evalConstFloatExpr` delegated only INTEGER leaves to `evalConstIntExpr` and had no float-const leaf arm, so the unified rule never saw the value. - program_index.zig: add `moduleConstFloat`/`moduleConstFloatFramed` — the f64 twin of `moduleConstInt` (same `isCountableConstType` gate, same cyclic- definition frame), recovering a numeric module const's value via `evalConstFloatExpr`. Add `lookupFloatName` to `ModuleConstCtx` and the `.identifier`/`.type_expr` leaf arms to `evalConstFloatExpr` that call it. Integer / integral-float leaves keep resolving through the existing `evalConstIntExpr` delegation, so the unified rule now applies to ANY compile-time-constant float expression — literal, int-const leaf, float-const leaf, and combinations — at every binding site. - lower.zig: add `Lowering.lookupFloatName` delegating to `moduleConstFloat`. Route `typedConstInitFits`' integral-fold check through `evalConstFloatExpr` + `floatToIntExact` (the SAME facility `foldComptimeFloatInit` uses) instead of the int-only `evalComptimeInt`, which folded leaf-by-leaf in i64 and so rejected an integral SUM built from a non-integral float leaf (`K : s64 : F + 1.5` = 4.0 now folds; `K : s64 : F + 0.25` errors). A LOCAL `::` const leaf is a scope ref (not in the const tables) so neither the int nor float evaluator folds it — float now matches int exactly there. Regression: examples/1146 (negative) + 0168 (positive) extended with float-const-leaf cases at local/field/param/const; unit test in program_index.test.zig covers the leaf resolution (F→2.5, F+0.25→2.75, F+1.5→4.0). specs.md + readme.md state the rule covers any compile-time-const float expression incl. float-typed const leaves. issues/0095 banner updated. Gate: zig build + zig build test green; 447 examples pass, 0 failed.
This commit is contained in:
@@ -2,54 +2,64 @@
|
|||||||
// flowing into an integer-typed binding FOLDS to its integer — the same
|
// flowing into an integer-typed binding FOLDS to its integer — the same
|
||||||
// `floatToIntExact` rule an array dimension / `$K: Count` already uses — across
|
// `floatToIntExact` rule an array dimension / `$K: Count` already uses — across
|
||||||
// a typed LOCAL, a struct FIELD default, a typed module CONST, and a function
|
// a typed LOCAL, a struct FIELD default, a typed module CONST, and a function
|
||||||
// PARAM default. It folds whether written as a float LITERAL (`4.0`) or a
|
// PARAM default. It folds whether written as a float LITERAL (`4.0`), an
|
||||||
// const-EXPRESSION (`M + 2.0`). The escape hatch (`xx` / `cast`) still TRUNCATES
|
// INT-const-EXPRESSION (`M + 2.0`, with `M :: 2`), or a FLOAT-const-LEAF
|
||||||
// any float, integral or not — including a non-integral const expression.
|
// expression whose sum is integral (`F + 1.5`, with `F : f64 : 2.5`, = 4.0).
|
||||||
|
// The escape hatch (`xx` / `cast`) still TRUNCATES any float, integral or not —
|
||||||
|
// including a non-integral const expression (`xx (M + 0.5)` / `xx (F + 0.25)`).
|
||||||
//
|
//
|
||||||
// Companion to the negative example 1146 (non-integral floats error).
|
// Companion to the negative example 1146 (non-integral floats error).
|
||||||
// Regression (issue 0095): a typed local/param/field silently truncated a float
|
// Regression (issue 0095): a typed local/param/field silently truncated a float
|
||||||
// initializer (`y : s64 = 1.5` → 1) with no diagnostic, and a non-integral const
|
// initializer (`y : s64 = 1.5` → 1) with no diagnostic; a non-integral const
|
||||||
// EXPRESSION (`M + 0.5`) truncated even when written through an int binding; the
|
// EXPRESSION (`M + 0.5`) and a non-integral float-const-LEAF expression
|
||||||
// rule now folds an integral float (literal or expression) and rejects a
|
// (`F + 0.25`) truncated even when written through an int binding; the rule now
|
||||||
// non-integral one.
|
// folds an integral float (literal, int-const expr, or float-const leaf) and
|
||||||
|
// rejects a non-integral one.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
M :: 2; // module const, for the const-EXPRESSION cases
|
M :: 2; // int module const, for the INT-const-EXPRESSION cases
|
||||||
|
F : f64 : 2.5; // float module const, for the FLOAT-const-LEAF cases
|
||||||
|
|
||||||
Box :: struct {
|
Box :: struct {
|
||||||
n : s64 = 4.0; // integral float field default → folds to 4
|
n : s64 = 4.0; // integral float field default → folds to 4
|
||||||
ne : s64 = M + 2.0; // integral float EXPRESSION field default → folds to 4
|
ne : s64 = M + 2.0; // integral int-const-EXPR field default → folds to 4
|
||||||
|
nf : s64 = F + 1.5; // integral float-const-LEAF field default → folds to 4
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefault :: (x : s64 = 6.0) -> s64 { return x; } // param default → 6
|
withDefault :: (x : s64 = 6.0) -> s64 { return x; } // param default → 6
|
||||||
|
withFlt :: (x : s64 = F + 1.5) -> s64 { return x; } // float-const-leaf param default → 4
|
||||||
|
|
||||||
K : s64 : 8.0; // integral float module const → folds to 8
|
K : s64 : 8.0; // integral float module const → folds to 8
|
||||||
|
KF : s64 : F + 1.5; // integral float-const-LEAF module const → folds to 4
|
||||||
|
|
||||||
main :: () {
|
main :: () {
|
||||||
// Typed local: integral float folds (literal + expression).
|
// Typed local: integral float folds (literal + int-const expr + float-const leaf).
|
||||||
z : s64 = 4.0;
|
z : s64 = 4.0;
|
||||||
ze : s64 = M + 2.0;
|
ze : s64 = M + 2.0;
|
||||||
print("local={} localExpr={}\n", z, ze);
|
zf : s64 = F + 1.5;
|
||||||
|
print("local={} localExpr={} localFlt={}\n", z, ze, zf);
|
||||||
|
|
||||||
// Negative integral float folds to its (negative) integer.
|
// Negative integral float folds to its (negative) integer.
|
||||||
neg : s64 = -2.0;
|
neg : s64 = -2.0;
|
||||||
print("neg={}\n", neg);
|
print("neg={}\n", neg);
|
||||||
|
|
||||||
// Struct field defaults fold (literal + expression).
|
// Struct field defaults fold (literal + int-const expr + float-const leaf).
|
||||||
b := Box.{};
|
b := Box.{};
|
||||||
print("field={} fieldExpr={}\n", b.n, b.ne);
|
print("field={} fieldExpr={} fieldFlt={}\n", b.n, b.ne, b.nf);
|
||||||
|
|
||||||
// Param default folds.
|
// Param defaults fold.
|
||||||
print("param={}\n", withDefault());
|
print("param={} paramFlt={}\n", withDefault(), withFlt());
|
||||||
|
|
||||||
// Module const folds (and can drive an array dimension: len 8).
|
// Module consts fold (and an integral float const can drive an array dim: len 8).
|
||||||
a : [K]s64 = ---;
|
a : [K]s64 = ---;
|
||||||
print("const={} len={}\n", K, a.len);
|
print("const={} constFlt={} len={}\n", K, KF, a.len);
|
||||||
|
|
||||||
// Explicit escape: `xx` / `cast` always truncate, integral or not —
|
// Explicit escape: `xx` / `cast` always truncate, integral or not —
|
||||||
// including a non-integral const EXPRESSION (`xx (M + 0.5)` → 2).
|
// including a non-integral const EXPRESSION (`xx (M + 0.5)` → 2) and a
|
||||||
|
// non-integral float-const-LEAF expression (`xx (F + 0.25)` → 2).
|
||||||
e : s64 = xx 4.9;
|
e : s64 = xx 4.9;
|
||||||
c : s64 = cast(s64) 1.5;
|
c : s64 = cast(s64) 1.5;
|
||||||
xc : s64 = xx (M + 0.5);
|
xc : s64 = xx (M + 0.5);
|
||||||
print("xx={} cast={} xxExpr={}\n", e, c, xc);
|
xf : s64 = xx (F + 0.25);
|
||||||
|
print("xx={} cast={} xxExpr={} xxFlt={}\n", e, c, xc, xf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,39 @@
|
|||||||
// silent truncation. The rule fires at a typed LOCAL initializer, a function
|
// silent truncation. The rule fires at a typed LOCAL initializer, a function
|
||||||
// PARAM default, and a struct FIELD default; each emits a narrowing diagnostic
|
// PARAM default, and a struct FIELD default; each emits a narrowing diagnostic
|
||||||
// at the offending float and aborts (exit 1). It fires whether the float is a
|
// at the offending float and aborts (exit 1). It fires whether the float is a
|
||||||
// LITERAL (`1.5`) or a compile-time const EXPRESSION (`M + 0.5`) — the latter is
|
// LITERAL (`1.5`), an INT-const-expression (`M + 0.5`, with `M :: 2`), or a
|
||||||
// the core of issue 0095, which previously slipped through and truncated to 2.
|
// FLOAT-const-leaf expression (`F + 0.25`, with `F : f64 : 2.5`, = 2.75) — all
|
||||||
// The fix is the integral-fold / non-integral-error rule shared with the
|
// three are the core of issue 0095, which previously slipped through and
|
||||||
// array-dimension path.
|
// truncated to 2. The fix is the integral-fold / non-integral-error rule shared
|
||||||
|
// with the array-dimension path, applied to ANY compile-time-constant float
|
||||||
|
// expression (literal, int-const leaf, float-const leaf, and combinations).
|
||||||
//
|
//
|
||||||
// The escape hatch stays open: `y : s64 = xx 1.5` (or `cast(s64) 1.5`)
|
// The escape hatch stays open: `y : s64 = xx 1.5` (or `cast(s64) 1.5`)
|
||||||
// truncates with no error — exercised on the POSITIVE side (example 0168).
|
// truncates with no error — exercised on the POSITIVE side (example 0168).
|
||||||
//
|
//
|
||||||
// Regression (issue 0095): `y : s64 = 1.5` silently truncated to 1, and
|
// Regression (issue 0095): `y : s64 = 1.5` silently truncated to 1,
|
||||||
// `y : s64 = M + 0.5` silently truncated to 2.
|
// `y : s64 = M + 0.5` to 2, and `y : s64 = F + 0.25` (float-const leaf) to 2.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
M :: 2; // module const, for the const-EXPRESSION cases
|
M :: 2; // int module const, for the INT-const-EXPRESSION cases
|
||||||
|
F : f64 : 2.5; // float module const, for the FLOAT-const-LEAF cases
|
||||||
|
|
||||||
Bad :: struct {
|
Bad :: struct {
|
||||||
f : s64 = 3.5; // non-integral float LITERAL field default → error
|
f : s64 = 3.5; // non-integral float LITERAL field default → error
|
||||||
fe : s64 = M + 0.5; // non-integral const-EXPRESSION field default → error
|
fe : s64 = M + 0.5; // non-integral int-const-EXPR field default → error
|
||||||
|
ff : s64 = F + 0.25; // non-integral float-const-LEAF field default → error
|
||||||
}
|
}
|
||||||
|
|
||||||
badLit :: (x : s64 = 2.5) -> s64 { return x; } // non-integral LITERAL param default → error
|
badLit :: (x : s64 = 2.5) -> s64 { return x; } // non-integral LITERAL param default → error
|
||||||
badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral const-EXPR param default → error
|
badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral int-const-EXPR param default → error
|
||||||
|
badFlt :: (x : s64 = F + 0.25) -> s64 { return x; } // non-integral float-const-LEAF param default → error
|
||||||
|
|
||||||
main :: () {
|
main :: () {
|
||||||
y : s64 = 1.5; // non-integral float LITERAL local → error
|
y : s64 = 1.5; // non-integral float LITERAL local → error
|
||||||
ye : s64 = M + 0.5; // non-integral const-EXPRESSION local → error
|
ye : s64 = M + 0.5; // non-integral int-const-EXPRESSION local → error
|
||||||
|
yf : s64 = F + 0.25; // non-integral float-const-LEAF local → error
|
||||||
b := Bad.{};
|
b := Bad.{};
|
||||||
print("{} {}\n", b.f, b.fe);
|
print("{} {} {}\n", b.f, b.fe, b.ff);
|
||||||
print("{} {}\n", badLit(), badExpr());
|
print("{} {} {}\n", badLit(), badExpr(), badFlt());
|
||||||
print("{} {}\n", y, ye);
|
print("{} {} {}\n", y, ye, yf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
local=4 localExpr=4
|
local=4 localExpr=4 localFlt=4
|
||||||
neg=-2
|
neg=-2
|
||||||
field=4 fieldExpr=4
|
field=4 fieldExpr=4 fieldFlt=4
|
||||||
param=6
|
param=6 paramFlt=4
|
||||||
const=8 len=8
|
const=8 constFlt=4 len=8
|
||||||
xx=4 cast=1 xxExpr=2
|
xx=4 cast=1 xxExpr=2 xxFlt=2
|
||||||
|
|||||||
@@ -1,35 +1,53 @@
|
|||||||
error: cannot implicitly narrow non-integral float '1.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
error: cannot implicitly narrow non-integral float '1.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:29:16
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:34:16
|
||||||
|
|
|
|
||||||
29 | y : s64 = 1.5; // non-integral float LITERAL local → error
|
34 | y : s64 = 1.5; // non-integral float LITERAL local → error
|
||||||
| ^^^
|
| ^^^
|
||||||
|
|
||||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:30:16
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:35:16
|
||||||
|
|
|
|
||||||
30 | ye : s64 = M + 0.5; // non-integral const-EXPRESSION local → error
|
35 | ye : s64 = M + 0.5; // non-integral int-const-EXPRESSION local → error
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
||||||
|
error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:36:16
|
||||||
|
|
|
||||||
|
36 | yf : s64 = F + 0.25; // non-integral float-const-LEAF local → error
|
||||||
|
| ^^^^^^^^
|
||||||
|
|
||||||
error: cannot implicitly narrow non-integral float '3.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
error: cannot implicitly narrow non-integral float '3.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:21:16
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:24:16
|
||||||
|
|
|
|
||||||
21 | f : s64 = 3.5; // non-integral float LITERAL field default → error
|
24 | f : s64 = 3.5; // non-integral float LITERAL field default → error
|
||||||
| ^^^
|
| ^^^
|
||||||
|
|
||||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:22:16
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:25:16
|
||||||
|
|
|
|
||||||
22 | fe : s64 = M + 0.5; // non-integral const-EXPRESSION field default → error
|
25 | fe : s64 = M + 0.5; // non-integral int-const-EXPR field default → error
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
||||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:25:23
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:26:16
|
||||||
|
|
|
|
||||||
25 | badLit :: (x : s64 = 2.5) -> s64 { return x; } // non-integral LITERAL param default → error
|
26 | ff : s64 = F + 0.25; // non-integral float-const-LEAF field default → error
|
||||||
|
| ^^^^^^^^
|
||||||
|
|
||||||
|
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:29:23
|
||||||
|
|
|
||||||
|
29 | badLit :: (x : s64 = 2.5) -> s64 { return x; } // non-integral LITERAL param default → error
|
||||||
| ^^^
|
| ^^^
|
||||||
|
|
||||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:26:23
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:30:23
|
||||||
|
|
|
|
||||||
26 | badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral const-EXPR param default → error
|
30 | badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral int-const-EXPR param default → error
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
||||||
|
error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:31:23
|
||||||
|
|
|
||||||
|
31 | badFlt :: (x : s64 = F + 0.25) -> s64 { return x; } // non-integral float-const-LEAF param default → error
|
||||||
|
| ^^^^^^^^
|
||||||
|
|||||||
@@ -42,15 +42,44 @@
|
|||||||
> reads `cannot implicitly narrow non-integral float …` instead of the stale
|
> reads `cannot implicitly narrow non-integral float …` instead of the stale
|
||||||
> `initializer is a float literal / floating-point expression`.
|
> `initializer is a float literal / floating-point expression`.
|
||||||
>
|
>
|
||||||
|
> **Completion (F0.11 attempt 3)** — attempt 2 resolved INT-const-expr leaves
|
||||||
|
> (`M + 0.5`, `M :: 2`), but a non-integral result via a FLOAT-const leaf
|
||||||
|
> (`F : f64 : 2.5; y : s64 = F + 0.25` = 2.75) still truncated silently:
|
||||||
|
> `evalConstFloatExpr` delegated only integer leaves to `evalConstIntExpr` and had
|
||||||
|
> no float-const leaf arm. Closed by completing the evaluator:
|
||||||
|
> - `program_index.moduleConstFloat` — the f64 twin of `moduleConstInt` (same
|
||||||
|
> `isCountableConstType` gate, same cyclic-definition frame), recovering a
|
||||||
|
> numeric module const's value through `evalConstFloatExpr`. A new
|
||||||
|
> `lookupFloatName` ctx method (on `Lowering` and `ModuleConstCtx`) surfaces a
|
||||||
|
> NON-INTEGRAL float const leaf; `evalConstFloatExpr` gained `.identifier` /
|
||||||
|
> `.type_expr` arms that call it. Integer / integral-float leaves keep resolving
|
||||||
|
> through the existing `evalConstIntExpr` delegation, so the unified rule now
|
||||||
|
> applies to ANY compile-time-constant float expression — literal, int-const
|
||||||
|
> leaf, float-const leaf, and combinations — at every binding site.
|
||||||
|
> - `typedConstInitFits` now judges integral-fold via `evalConstFloatExpr` +
|
||||||
|
> `floatToIntExact` (the SAME facility `foldComptimeFloatInit` uses) instead of
|
||||||
|
> the int-only `evalComptimeInt`, which folded leaf-by-leaf in `i64` and so
|
||||||
|
> rejected an integral SUM built from a non-integral float leaf
|
||||||
|
> (`K : s64 : F + 1.5` = 4.0). Integral float-const-leaf consts now FOLD;
|
||||||
|
> non-integral ones still error with the unified wording.
|
||||||
|
> - Out of scope (consistent with the int evaluator): a LOCAL `::` const leaf is
|
||||||
|
> resolved as a scope ref, not through the const tables, so neither
|
||||||
|
> `evalConstIntExpr` nor `evalConstFloatExpr` folds it — a local `M : s64 : 2`
|
||||||
|
> in `M + 0.5` and a local `F : f64 : 2.5` in `F + 0.25` both still truncate
|
||||||
|
> identically. Float now matches int exactly at that boundary.
|
||||||
|
>
|
||||||
> Regression tests: `examples/0168-types-integral-float-to-int.sx` (positive —
|
> Regression tests: `examples/0168-types-integral-float-to-int.sx` (positive —
|
||||||
> local/field/param/const fold, integral const-EXPRESSION (`M + 2.0`) folds,
|
> local/field/param/const fold, integral int-const-EXPRESSION (`M + 2.0`) AND
|
||||||
> `xx`/`cast` truncate incl. `xx (M + 0.5)`), `examples/1146-diagnostics-
|
> float-const-LEAF (`F + 1.5`, `F : f64 : 2.5`) fold at local/field/param/const,
|
||||||
> nonintegral-float-to-int.sx` (negative — non-integral LITERAL and const-
|
> `xx`/`cast` truncate incl. `xx (M + 0.5)` / `xx (F + 0.25)`),
|
||||||
> EXPRESSION error at local/param/field), the integral-float const cases in
|
> `examples/1146-diagnostics-nonintegral-float-to-int.sx` (negative —
|
||||||
|
> non-integral LITERAL, int-const-EXPRESSION (`M + 0.5`), AND float-const-LEAF
|
||||||
|
> (`F + 0.25`) error at local/param/field), the integral-float const cases in
|
||||||
> `examples/0162-types-typed-module-const-roundtrip.sx`, and the aligned const
|
> `examples/0162-types-typed-module-const-roundtrip.sx`, and the aligned const
|
||||||
> diagnostic in `examples/1143-diagnostics-typed-module-const-mismatch.sx`
|
> diagnostic in `examples/1143-diagnostics-typed-module-const-mismatch.sx`
|
||||||
> (G / BAD / BAD2 stay errors with the new wording). Unit:
|
> (G / BAD / BAD2 stay errors with the new wording). Unit:
|
||||||
> `program_index.test.zig` "evalConstFloatExpr folds comptime float expressions".
|
> `program_index.test.zig` "evalConstFloatExpr folds comptime float expressions"
|
||||||
|
> (covers the float-const leaf: `F` → 2.5, `F + 0.25` → 2.75, `F + 1.5` → 4.0).
|
||||||
|
|
||||||
## Symptom
|
## Symptom
|
||||||
A typed LOCAL (and likely typed param/field) silently truncates a floating-point
|
A typed LOCAL (and likely typed param/field) silently truncates a floating-point
|
||||||
|
|||||||
17
readme.md
17
readme.md
@@ -127,13 +127,16 @@ while `F : f64 : M + 0.5` folds to `2.5`.
|
|||||||
integer-typed binding *without* a cast follows the same integral-fold rule an
|
integer-typed binding *without* a cast follows the same integral-fold rule an
|
||||||
array dimension uses: an **integral** compile-time float folds to its integer, a
|
array dimension uses: an **integral** compile-time float folds to its integer, a
|
||||||
**non-integral** one is a compile error. It holds whether the value is a literal
|
**non-integral** one is a compile error. It holds whether the value is a literal
|
||||||
or a const expression, and is uniform across a typed local, a parameter default,
|
or *any* compile-time-constant float expression — including one that references a
|
||||||
a struct field default, a call argument, and a typed constant — `y : s64 = 4.0`,
|
float-typed const (`F : f64 : 2.5; y : s64 = F + 1.5` → `4`) — and is uniform
|
||||||
`K : s64 : 4.0`, and `y : s64 = M + 2.0` all give `4`, while `y : s64 = 1.5`,
|
across a typed local, a parameter default, a struct field default, a call
|
||||||
`N : s64 : 1.5`, and `y : s64 = M + 0.5` all error (one wording everywhere:
|
argument, and a typed constant — `y : s64 = 4.0`, `K : s64 : 4.0`, and
|
||||||
`cannot implicitly narrow non-integral float …`). An explicit `xx` / `cast(s64)`
|
`y : s64 = M + 2.0` all give `4`, while `y : s64 = 1.5`, `N : s64 : 1.5`,
|
||||||
is the escape hatch and always truncates (`y : s64 = xx 1.5` → `1`,
|
`y : s64 = M + 0.5`, and `y : s64 = F + 0.25` (= `2.75`) all error (one wording
|
||||||
`y : s64 = xx (M + 0.5)` → `2`); a genuine runtime float is likewise unaffected.
|
everywhere: `cannot implicitly narrow non-integral float …`). An explicit
|
||||||
|
`xx` / `cast(s64)` is the escape hatch and always truncates (`y : s64 = xx 1.5` →
|
||||||
|
`1`, `y : s64 = xx (M + 0.5)` → `2`); a genuine runtime float is likewise
|
||||||
|
unaffected.
|
||||||
|
|
||||||
Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and a *bare*
|
Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and a *bare*
|
||||||
spelling can't be used as an identifier at a **value-binding or declaration-name**
|
spelling can't be used as an identifier at a **value-binding or declaration-name**
|
||||||
|
|||||||
7
specs.md
7
specs.md
@@ -1428,10 +1428,13 @@ array dimension / lane count uses (see "Array dimensions are integral", §2):
|
|||||||
|
|
||||||
- An **integral** compile-time float **folds** to its integer, whether written
|
- An **integral** compile-time float **folds** to its integer, whether written
|
||||||
as a literal or a const expression: `y : s64 = 4.0` ≡ `y : s64 = 4`,
|
as a literal or a const expression: `y : s64 = 4.0` ≡ `y : s64 = 4`,
|
||||||
`n : s64 = -2.0` ≡ `-2`, `y : s64 = M + 2.0` → 4 (`M :: 2`).
|
`n : s64 = -2.0` ≡ `-2`, `y : s64 = M + 2.0` → 4 (`M :: 2`). A const expression
|
||||||
|
here is *any* compile-time-constant float expression — an integer-const leaf
|
||||||
|
(`M + 2.0`), a float-typed const leaf (`F : f64 : 2.5; y : s64 = F + 1.5` → 4),
|
||||||
|
or any combination of them.
|
||||||
- A **non-integral** compile-time float — literal OR const expression — is a
|
- A **non-integral** compile-time float — literal OR const expression — is a
|
||||||
**compile error** with one uniform wording at every site:
|
**compile error** with one uniform wording at every site:
|
||||||
`y : s64 = 1.5` and `y : s64 = M + 0.5` both →
|
`y : s64 = 1.5`, `y : s64 = M + 0.5`, and `y : s64 = F + 0.25` (= 2.75) all →
|
||||||
"cannot implicitly narrow non-integral float '…' to 's64'; use an explicit
|
"cannot implicitly narrow non-integral float '…' to 's64'; use an explicit
|
||||||
cast (`xx`/`cast`)".
|
cast (`xx`/`cast`)".
|
||||||
- This applies uniformly to a typed **local**, a function **param default**, a
|
- This applies uniformly to a typed **local**, a function **param default**, a
|
||||||
|
|||||||
@@ -988,15 +988,22 @@ pub const Lowering = struct {
|
|||||||
/// `B : s64 : true`.
|
/// `B : s64 : true`.
|
||||||
fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
|
fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
|
||||||
// An INTEGER-annotated constant accepts a compile-time INTEGRAL float —
|
// An INTEGER-annotated constant accepts a compile-time INTEGRAL float —
|
||||||
// a literal (`K : s64 : 4.0`) or an expression that folds to an integer
|
// a literal (`K : s64 : 4.0`), an int-leaf expression (`K : s64 : M + 2.0`
|
||||||
// (`K : s64 : M + 2.0` → 4) — via the SAME `evalConstIntExpr` /
|
// → 4), or a float-const-leaf expression whose SUM is integral
|
||||||
// `floatToIntExact` the array-dim path uses. A non-integral float
|
// (`F : f64 : 2.5; K : s64 : F + 1.5` → 4). Integrality is judged on the
|
||||||
// (`1.5`, `M + 0.5`) folds to null and falls through to the rejecting
|
// FLOAT fold (`evalConstFloatExpr` + `floatToIntExact`) — the SAME facility
|
||||||
// checks below, matching the typed-local rule.
|
// the typed-local path (`foldComptimeFloatInit`) uses — not the int-only
|
||||||
|
// folder, which folds leaf-by-leaf in `i64` and so misses an integral SUM
|
||||||
|
// built from a non-integral float leaf. A non-integral fold (`1.5`,
|
||||||
|
// `M + 0.5`, `F + 0.25`) yields null here and falls through to the
|
||||||
|
// rejecting checks below, where `registerTypedModuleConst` emits the
|
||||||
|
// unified narrowing diagnostic.
|
||||||
if (self.isIntEx(dst_ty)) {
|
if (self.isIntEx(dst_ty)) {
|
||||||
switch (value.data) {
|
switch (value.data) {
|
||||||
.float_literal, .binary_op, .unary_op => {
|
.float_literal, .binary_op, .unary_op => {
|
||||||
if (self.evalComptimeInt(value) != null) return true;
|
if (program_index_mod.evalConstFloatExpr(value, self)) |fv| {
|
||||||
|
if (program_index_mod.floatToIntExact(fv) != null) return true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
@@ -12113,6 +12120,18 @@ pub const Lowering = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Float-valued leaf for the shared float-expression evaluator: a name bound
|
||||||
|
/// to a NUMERIC module const whose compile-time value is a (non-integral)
|
||||||
|
/// float — the FLOAT counterpart of `lookupDimName`, routed through the SAME
|
||||||
|
/// `module_const_map` so the unified narrowing rule resolves a float-const
|
||||||
|
/// leaf (`F : f64 : 2.5`) exactly as it resolves an int-const leaf. Integer /
|
||||||
|
/// integral-float leaves and comptime int bindings are already resolved by the
|
||||||
|
/// `evalConstIntExpr` delegation inside `evalConstFloatExpr`; this surfaces the
|
||||||
|
/// non-integral float const so the rule can reject it.
|
||||||
|
pub fn lookupFloatName(self: *Lowering, name: []const u8) ?f64 {
|
||||||
|
return program_index_mod.moduleConstFloat(&self.program_index.module_const_map, &self.module.types, name);
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a name to a compile-time integer across the three const tables.
|
/// Resolve a name to a compile-time integer across the three const tables.
|
||||||
fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 {
|
fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 {
|
||||||
if (self.comptime_constants.get(name)) |cv| switch (cv) {
|
if (self.comptime_constants.get(name)) |cv| switch (cv) {
|
||||||
|
|||||||
@@ -111,6 +111,14 @@ const DimCtx = struct {
|
|||||||
if (std.mem.eql(u8, name, "xs")) return 3;
|
if (std.mem.eql(u8, name, "xs")) return 3;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// `F` stands in for a NON-INTEGRAL float module const (`F : f64 : 2.5`): the
|
||||||
|
// int folder cannot resolve it, so only the float-leaf lookup surfaces it.
|
||||||
|
// Integer consts (`M`/`N`) are resolved by the int delegation and never reach
|
||||||
|
// this arm; `Z` is genuinely runtime.
|
||||||
|
pub fn lookupFloatName(_: DimCtx, name: []const u8) ?f64 {
|
||||||
|
if (std.mem.eql(u8, name, "F")) return 2.5;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn nLit(v: i64) ast.Node {
|
fn nLit(v: i64) ast.Node {
|
||||||
@@ -345,6 +353,21 @@ test "evalConstFloatExpr folds comptime float expressions, halts on runtime leav
|
|||||||
var neg = nNeg(&mp);
|
var neg = nNeg(&mp);
|
||||||
try std.testing.expectEqual(@as(?f64, -4.5), eval(&neg, ctx));
|
try std.testing.expectEqual(@as(?f64, -4.5), eval(&neg, ctx));
|
||||||
|
|
||||||
|
// A NON-INTEGRAL float-const leaf (`F : f64 : 2.5`) resolves through the
|
||||||
|
// float-leaf lookup — the int folder cannot fold it (2.5 is not integral), so
|
||||||
|
// an expression like `F + 0.25` (= 2.75) is now recognised as a compile-time
|
||||||
|
// float and rejected by the narrowing rule instead of silently truncating;
|
||||||
|
// `F + 1.5` (= 4.0) is integral and folds. This completes the evaluator for
|
||||||
|
// float-const-leaf expressions (issue 0095, attempt 3).
|
||||||
|
var f = nIdent("F");
|
||||||
|
var quarter = nFloat(0.25);
|
||||||
|
var three_half = nFloat(1.5);
|
||||||
|
var fq = nBin(.add, &f, &quarter);
|
||||||
|
var fh = nBin(.add, &f, &three_half);
|
||||||
|
try std.testing.expectEqual(@as(?f64, 2.5), eval(&f, ctx));
|
||||||
|
try std.testing.expectEqual(@as(?f64, 2.75), eval(&fq, ctx));
|
||||||
|
try std.testing.expectEqual(@as(?f64, 4.0), eval(&fh, ctx));
|
||||||
|
|
||||||
// A runtime operand poisons the whole fold; a non-arithmetic operator and a
|
// A runtime operand poisons the whole fold; a non-arithmetic operator and a
|
||||||
// float division by zero are not compile-time float leaves → null.
|
// float division by zero are not compile-time float leaves → null.
|
||||||
var zp = nBin(.add, &z, &half);
|
var zp = nBin(.add, &z, &half);
|
||||||
|
|||||||
@@ -99,6 +99,13 @@ const ModuleConstCtx = struct {
|
|||||||
pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 {
|
pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
/// Float counterpart of `lookupDimName`, so `evalConstFloatExpr` resolves a
|
||||||
|
/// float-const leaf whose value references another const
|
||||||
|
/// (`G : f64 : 2.0; F : f64 : G + 0.5`) recursively through the SAME
|
||||||
|
/// cycle-guarded frame.
|
||||||
|
pub fn lookupFloatName(self: ModuleConstCtx, name: []const u8) ?f64 {
|
||||||
|
return moduleConstFloatFramed(self.consts, self.table, name, self.frame);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A module const may serve as an integer COUNT only when its DECLARED type is
|
/// A module const may serve as an integer COUNT only when its DECLARED type is
|
||||||
@@ -144,6 +151,28 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), table:
|
|||||||
return moduleConstIntFramed(consts, table, name, null);
|
return moduleConstIntFramed(consts, table, name, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// FLOAT counterpart of `moduleConstInt`: a name bound to a NUMERIC module const
|
||||||
|
/// → its compile-time `f64` value (`F : f64 : 2.5` → 2.5), else null. Mirrors
|
||||||
|
/// `moduleConstIntFramed` exactly — same `isCountableConstType` gate, same cyclic-
|
||||||
|
/// definition frame — but recovers the value through `evalConstFloatExpr`, so the
|
||||||
|
/// unified float→int narrowing rule resolves a NON-INTEGRAL float-const leaf
|
||||||
|
/// (`y : s64 = F + 0.25`) the same way the int folder resolves an int-const leaf
|
||||||
|
/// (`M :: 2; y : s64 = M + 0.5`). An integral float / integer const folds through
|
||||||
|
/// the int path inside `evalConstFloatExpr` and never reaches the leaf arm that
|
||||||
|
/// calls this; this surfaces the genuinely non-integral float so `floatToIntExact`
|
||||||
|
/// can reject it.
|
||||||
|
fn moduleConstFloatFramed(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, parent: ?*const ModuleConstFrame) ?f64 {
|
||||||
|
if (moduleConstFrameContains(parent, name)) return null;
|
||||||
|
const ci = consts.get(name) orelse return null;
|
||||||
|
if (!isCountableConstType(table, ci.ty)) return null;
|
||||||
|
var frame = ModuleConstFrame{ .name = name, .parent = parent };
|
||||||
|
return evalConstFloatExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn moduleConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8) ?f64 {
|
||||||
|
return moduleConstFloatFramed(consts, table, name, null);
|
||||||
|
}
|
||||||
|
|
||||||
/// Evaluate a constant integer expression to its value. THE single
|
/// Evaluate a constant integer expression to its value. THE single
|
||||||
/// integer-expression folder for the compiler — array dimensions (`[N]T`,
|
/// integer-expression folder for the compiler — array dimensions (`[N]T`,
|
||||||
/// `[M + 1]T`), Vector lane counts (`Vector(N, f32)`), generic value-param
|
/// `[M + 1]T`), Vector lane counts (`Vector(N, f32)`), generic value-param
|
||||||
@@ -228,9 +257,18 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
|
|||||||
/// An all-integer-foldable subtree is delegated to `evalConstIntExpr` (so module
|
/// An all-integer-foldable subtree is delegated to `evalConstIntExpr` (so module
|
||||||
/// / comptime consts, `<IntType>.min`/`.max`, and integer arithmetic resolve
|
/// / comptime consts, `<IntType>.min`/`.max`, and integer arithmetic resolve
|
||||||
/// through the SINGLE int folder — no parallel integer logic here); only the
|
/// through the SINGLE int folder — no parallel integer logic here); only the
|
||||||
/// genuinely float-producing shapes — a float literal, a unary negate, and
|
/// genuinely float-producing shapes — a float literal, a NON-INTEGRAL float-const
|
||||||
/// `+ - * /` arithmetic involving a float — are evaluated here in `f64`. A `%`,
|
/// leaf, a unary negate, and `+ - * /` arithmetic involving a float — are
|
||||||
/// comparison, or any other shape is not a compile-time float leaf → null.
|
/// evaluated here in `f64`. A `%`, comparison, or any other shape is not a
|
||||||
|
/// compile-time float leaf → null.
|
||||||
|
///
|
||||||
|
/// A NAMED-const leaf resolves through `ctx.lookupFloatName`, the float twin of
|
||||||
|
/// the `lookupDimName` the int folder uses: a numeric module const whose value is
|
||||||
|
/// a non-integral float (`F : f64 : 2.5`) surfaces here so `F + 0.25` (= 2.75) is
|
||||||
|
/// recognised as a compile-time float and rejected by the narrowing rule, exactly
|
||||||
|
/// as `M + 0.5` (with `M :: 2`) already is. An INTEGRAL float / integer const
|
||||||
|
/// (`K : f64 : 4.0`, `M :: 2`) is resolved by the `evalConstIntExpr` delegation
|
||||||
|
/// above and never reaches the leaf arm.
|
||||||
pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 {
|
pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 {
|
||||||
// Delegate any integer-foldable subtree (incl. an INTEGRAL float like `4.0`
|
// Delegate any integer-foldable subtree (incl. an INTEGRAL float like `4.0`
|
||||||
// / `M + 2.0`) to the single int folder, then promote — keeps named consts
|
// / `M + 2.0`) to the single int folder, then promote — keeps named consts
|
||||||
@@ -238,6 +276,10 @@ pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 {
|
|||||||
if (evalConstIntExpr(node, ctx)) |iv| return @floatFromInt(iv);
|
if (evalConstIntExpr(node, ctx)) |iv| return @floatFromInt(iv);
|
||||||
return switch (node.data) {
|
return switch (node.data) {
|
||||||
.float_literal => |lit| lit.value,
|
.float_literal => |lit| lit.value,
|
||||||
|
// A name bound to a numeric module const whose value is a non-integral
|
||||||
|
// float (the integral / integer cases were caught by the int delegation).
|
||||||
|
.identifier => |id| ctx.lookupFloatName(id.name),
|
||||||
|
.type_expr => |te| ctx.lookupFloatName(te.name),
|
||||||
.unary_op => |u| switch (u.op) {
|
.unary_op => |u| switch (u.op) {
|
||||||
.negate => {
|
.negate => {
|
||||||
const v = evalConstFloatExpr(u.operand, ctx) orelse return null;
|
const v = evalConstFloatExpr(u.operand, ctx) orelse return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user