diff --git a/examples/0168-types-integral-float-to-int.sx b/examples/0168-types-integral-float-to-int.sx index fefab84..da44828 100644 --- a/examples/0168-types-integral-float-to-int.sx +++ b/examples/0168-types-integral-float-to-int.sx @@ -2,54 +2,64 @@ // flowing into an integer-typed binding FOLDS to its integer — the same // `floatToIntExact` rule an array dimension / `$K: Count` already uses — across // 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 -// const-EXPRESSION (`M + 2.0`). The escape hatch (`xx` / `cast`) still TRUNCATES -// any float, integral or not — including a non-integral const expression. +// PARAM default. It folds whether written as a float LITERAL (`4.0`), an +// INT-const-EXPRESSION (`M + 2.0`, with `M :: 2`), or a FLOAT-const-LEAF +// 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). // 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 -// EXPRESSION (`M + 0.5`) truncated even when written through an int binding; the -// rule now folds an integral float (literal or expression) and rejects a -// non-integral one. +// initializer (`y : s64 = 1.5` → 1) with no diagnostic; a non-integral const +// EXPRESSION (`M + 0.5`) and a non-integral float-const-LEAF expression +// (`F + 0.25`) truncated even when written through an int binding; the rule now +// folds an integral float (literal, int-const expr, or float-const leaf) and +// rejects a non-integral one. #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 { 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 :: () { - // Typed local: integral float folds (literal + expression). + // Typed local: integral float folds (literal + int-const expr + float-const leaf). z : s64 = 4.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. neg : s64 = -2.0; print("neg={}\n", neg); - // Struct field defaults fold (literal + expression). + // Struct field defaults fold (literal + int-const expr + float-const leaf). b := Box.{}; - print("field={} fieldExpr={}\n", b.n, b.ne); + print("field={} fieldExpr={} fieldFlt={}\n", b.n, b.ne, b.nf); - // Param default folds. - print("param={}\n", withDefault()); + // Param defaults fold. + 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 = ---; - 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 — - // 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; c : s64 = cast(s64) 1.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); } diff --git a/examples/1146-diagnostics-nonintegral-float-to-int.sx b/examples/1146-diagnostics-nonintegral-float-to-int.sx index 9da3ed4..198c7bc 100644 --- a/examples/1146-diagnostics-nonintegral-float-to-int.sx +++ b/examples/1146-diagnostics-nonintegral-float-to-int.sx @@ -3,33 +3,39 @@ // silent truncation. The rule fires at a typed LOCAL initializer, a function // 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 -// LITERAL (`1.5`) or a compile-time const EXPRESSION (`M + 0.5`) — the latter is -// the core of issue 0095, which previously slipped through and truncated to 2. -// The fix is the integral-fold / non-integral-error rule shared with the -// array-dimension path. +// LITERAL (`1.5`), an INT-const-expression (`M + 0.5`, with `M :: 2`), or a +// FLOAT-const-leaf expression (`F + 0.25`, with `F : f64 : 2.5`, = 2.75) — all +// three are the core of issue 0095, which previously slipped through and +// 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`) // truncates with no error — exercised on the POSITIVE side (example 0168). // -// Regression (issue 0095): `y : s64 = 1.5` silently truncated to 1, and -// `y : s64 = M + 0.5` silently truncated to 2. +// Regression (issue 0095): `y : s64 = 1.5` silently truncated to 1, +// `y : s64 = M + 0.5` to 2, and `y : s64 = F + 0.25` (float-const leaf) to 2. #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 { 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 -badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral const-EXPR 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 int-const-EXPR param default → error +badFlt :: (x : s64 = F + 0.25) -> s64 { return x; } // non-integral float-const-LEAF param default → error main :: () { 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.{}; - print("{} {}\n", b.f, b.fe); - print("{} {}\n", badLit(), badExpr()); - print("{} {}\n", y, ye); + print("{} {} {}\n", b.f, b.fe, b.ff); + print("{} {} {}\n", badLit(), badExpr(), badFlt()); + print("{} {} {}\n", y, ye, yf); } diff --git a/examples/expected/0168-types-integral-float-to-int.stdout b/examples/expected/0168-types-integral-float-to-int.stdout index 45a5d34..4ab124a 100644 --- a/examples/expected/0168-types-integral-float-to-int.stdout +++ b/examples/expected/0168-types-integral-float-to-int.stdout @@ -1,6 +1,6 @@ -local=4 localExpr=4 +local=4 localExpr=4 localFlt=4 neg=-2 -field=4 fieldExpr=4 -param=6 -const=8 len=8 -xx=4 cast=1 xxExpr=2 +field=4 fieldExpr=4 fieldFlt=4 +param=6 paramFlt=4 +const=8 constFlt=4 len=8 +xx=4 cast=1 xxExpr=2 xxFlt=2 diff --git a/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr b/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr index 067988f..d1558a0 100644 --- a/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr +++ b/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr @@ -1,35 +1,53 @@ 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`) - --> 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`) - --> 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`) - --> 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`) - --> examples/1146-diagnostics-nonintegral-float-to-int.sx:25:23 +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: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`) - --> 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 + | ^^^^^^^^ diff --git a/issues/0095-typed-local-float-int-narrowing.md b/issues/0095-typed-local-float-int-narrowing.md index 1d0a0c7..b78cf18 100644 --- a/issues/0095-typed-local-float-int-narrowing.md +++ b/issues/0095-typed-local-float-int-narrowing.md @@ -42,15 +42,44 @@ > reads `cannot implicitly narrow non-integral float …` instead of the stale > `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 — -> local/field/param/const fold, integral const-EXPRESSION (`M + 2.0`) folds, -> `xx`/`cast` truncate incl. `xx (M + 0.5)`), `examples/1146-diagnostics- -> nonintegral-float-to-int.sx` (negative — non-integral LITERAL and const- -> EXPRESSION error at local/param/field), the integral-float const cases in +> local/field/param/const fold, integral int-const-EXPRESSION (`M + 2.0`) AND +> float-const-LEAF (`F + 1.5`, `F : f64 : 2.5`) fold at local/field/param/const, +> `xx`/`cast` truncate incl. `xx (M + 0.5)` / `xx (F + 0.25)`), +> `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 > diagnostic in `examples/1143-diagnostics-typed-module-const-mismatch.sx` > (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 A typed LOCAL (and likely typed param/field) silently truncates a floating-point diff --git a/readme.md b/readme.md index b16cc0d..cc788e4 100644 --- a/readme.md +++ b/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 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 -or a const expression, and is uniform across a typed local, a parameter default, -a struct field default, a call argument, and a typed constant — `y : s64 = 4.0`, -`K : s64 : 4.0`, and `y : s64 = M + 2.0` all give `4`, while `y : s64 = 1.5`, -`N : s64 : 1.5`, and `y : s64 = M + 0.5` all error (one wording 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. +or *any* compile-time-constant float expression — including one that references a +float-typed const (`F : f64 : 2.5; y : s64 = F + 1.5` → `4`) — and is uniform +across a typed local, a parameter default, a struct field default, a call +argument, and a typed constant — `y : s64 = 4.0`, `K : s64 : 4.0`, and +`y : s64 = M + 2.0` all give `4`, while `y : s64 = 1.5`, `N : s64 : 1.5`, +`y : s64 = M + 0.5`, and `y : s64 = F + 0.25` (= `2.75`) all error (one wording +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* spelling can't be used as an identifier at a **value-binding or declaration-name** diff --git a/specs.md b/specs.md index 3765425..abfd582 100644 --- a/specs.md +++ b/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 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 **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 cast (`xx`/`cast`)". - This applies uniformly to a typed **local**, a function **param default**, a diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 36928af..c45a985 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -988,15 +988,22 @@ pub const Lowering = struct { /// `B : s64 : true`. fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool { // An INTEGER-annotated constant accepts a compile-time INTEGRAL float — - // a literal (`K : s64 : 4.0`) or an expression that folds to an integer - // (`K : s64 : M + 2.0` → 4) — via the SAME `evalConstIntExpr` / - // `floatToIntExact` the array-dim path uses. A non-integral float - // (`1.5`, `M + 0.5`) folds to null and falls through to the rejecting - // checks below, matching the typed-local rule. + // a literal (`K : s64 : 4.0`), an int-leaf expression (`K : s64 : M + 2.0` + // → 4), or a float-const-leaf expression whose SUM is integral + // (`F : f64 : 2.5; K : s64 : F + 1.5` → 4). Integrality is judged on the + // FLOAT fold (`evalConstFloatExpr` + `floatToIntExact`) — the SAME facility + // 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)) { switch (value.data) { .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 => {}, } @@ -12113,6 +12120,18 @@ pub const Lowering = struct { 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. fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 { if (self.comptime_constants.get(name)) |cv| switch (cv) { diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index 519108f..1820c86 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -111,6 +111,14 @@ const DimCtx = struct { if (std.mem.eql(u8, name, "xs")) return 3; 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 { @@ -345,6 +353,21 @@ test "evalConstFloatExpr folds comptime float expressions, halts on runtime leav var neg = nNeg(&mp); 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 // float division by zero are not compile-time float leaves → null. var zp = nBin(.add, &z, &half); diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index bd6fed9..b0ad404 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -99,6 +99,13 @@ const ModuleConstCtx = struct { pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 { 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 @@ -144,6 +151,28 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), table: 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 /// integer-expression folder for the compiler — array dimensions (`[N]T`, /// `[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 /// / comptime consts, `.min`/`.max`, and integer arithmetic resolve /// through the SINGLE int folder — no parallel integer logic here); only the -/// genuinely float-producing shapes — a float literal, a unary negate, and -/// `+ - * /` arithmetic involving a float — are evaluated here in `f64`. A `%`, -/// comparison, or any other shape is not a compile-time float leaf → null. +/// genuinely float-producing shapes — a float literal, a NON-INTEGRAL float-const +/// leaf, a unary negate, and `+ - * /` arithmetic involving a float — are +/// 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 { // 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 @@ -238,6 +276,10 @@ pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 { if (evalConstIntExpr(node, ctx)) |iv| return @floatFromInt(iv); return switch (node.data) { .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) { .negate => { const v = evalConstFloatExpr(u.operand, ctx) orelse return null;