diff --git a/examples/0168-types-integral-float-to-int.sx b/examples/0168-types-integral-float-to-int.sx index 3257503..fefab84 100644 --- a/examples/0168-types-integral-float-to-int.sx +++ b/examples/0168-types-integral-float-to-int.sx @@ -2,17 +2,23 @@ // 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. The escape hatch (`xx` / `cast`) still TRUNCATES any float, -// integral or not. +// 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. // // 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; the rule now folds an -// integral float and rejects a non-integral one. +// 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. #import "modules/std.sx"; +M :: 2; // module const, for the const-EXPRESSION cases + 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 } withDefault :: (x : s64 = 6.0) -> s64 { return x; } // param default → 6 @@ -20,17 +26,18 @@ withDefault :: (x : s64 = 6.0) -> s64 { return x; } // param default → 6 K : s64 : 8.0; // integral float module const → folds to 8 main :: () { - // Typed local: integral float folds. + // Typed local: integral float folds (literal + expression). z : s64 = 4.0; - print("local={}\n", z); + ze : s64 = M + 2.0; + print("local={} localExpr={}\n", z, ze); // Negative integral float folds to its (negative) integer. neg : s64 = -2.0; print("neg={}\n", neg); - // Struct field default folds. + // Struct field defaults fold (literal + expression). b := Box.{}; - print("field={}\n", b.n); + print("field={} fieldExpr={}\n", b.n, b.ne); // Param default folds. print("param={}\n", withDefault()); @@ -39,8 +46,10 @@ main :: () { a : [K]s64 = ---; print("const={} len={}\n", K, 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). e : s64 = xx 4.9; c : s64 = cast(s64) 1.5; - print("xx={} cast={}\n", e, c); + xc : s64 = xx (M + 0.5); + print("xx={} cast={} xxExpr={}\n", e, c, xc); } diff --git a/examples/1146-diagnostics-nonintegral-float-to-int.sx b/examples/1146-diagnostics-nonintegral-float-to-int.sx index eca05ed..9da3ed4 100644 --- a/examples/1146-diagnostics-nonintegral-float-to-int.sx +++ b/examples/1146-diagnostics-nonintegral-float-to-int.sx @@ -2,25 +2,34 @@ // implicitly narrowing to an integer-typed binding is a COMPILE ERROR — not a // 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). The fix is the integral-fold / -// non-integral-error rule shared with the array-dimension path. +// 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. // // 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. +// Regression (issue 0095): `y : s64 = 1.5` silently truncated to 1, and +// `y : s64 = M + 0.5` silently truncated to 2. #import "modules/std.sx"; +M :: 2; // module const, for the const-EXPRESSION cases + Bad :: struct { - f : s64 = 3.5; // non-integral 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 } -badDefault :: (x : s64 = 2.5) -> s64 { return x; } // non-integral 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 main :: () { - y : s64 = 1.5; // non-integral local initializer → error + y : s64 = 1.5; // non-integral float LITERAL local → error + ye : s64 = M + 0.5; // non-integral const-EXPRESSION local → error b := Bad.{}; - print("{}\n", b.f); - print("{}\n", badDefault()); - print("{}\n", y); + print("{} {}\n", b.f, b.fe); + print("{} {}\n", badLit(), badExpr()); + print("{} {}\n", y, ye); } diff --git a/examples/expected/0168-types-integral-float-to-int.stdout b/examples/expected/0168-types-integral-float-to-int.stdout index 3513b0c..45a5d34 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 +local=4 localExpr=4 neg=-2 -field=4 +field=4 fieldExpr=4 param=6 const=8 len=8 -xx=4 cast=1 +xx=4 cast=1 xxExpr=2 diff --git a/examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr b/examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr index 8b93822..be2e190 100644 --- a/examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr +++ b/examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr @@ -16,7 +16,7 @@ error: type mismatch: constant 'B' is declared 's64' but its initializer is a bo 26 | B : s64 : true; // boolean literal where an integer is annotated | ^^^^ -error: type mismatch: constant 'G' is declared 's64' but its initializer is a float literal +error: cannot implicitly narrow non-integral float '1.5' to 's64'; use an explicit cast (`xx`/`cast`) --> examples/1143-diagnostics-typed-module-const-mismatch.sx:27:15 | 27 | G : s64 : 1.5; // float literal where an integer is annotated @@ -34,13 +34,13 @@ error: type mismatch: constant 'V' is declared 'string' but its initializer is a 29 | V : string : -M; // integer (unary) expression where a string is annotated | ^^ -error: type mismatch: constant 'BAD' is declared 's64' but its initializer is a floating-point expression +error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`) --> examples/1143-diagnostics-typed-module-const-mismatch.sx:30:15 | 30 | BAD : s64 : M + 0.5; // mixed int+float (int LHS) → f64, rejected vs s64 | ^^^^^^^ -error: type mismatch: constant 'BAD2' is declared 's64' but its initializer is a floating-point expression +error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`) --> examples/1143-diagnostics-typed-module-const-mismatch.sx:31:15 | 31 | BAD2 : s64 : 0.5 + M; // mixed float+int (float LHS) → f64, rejected vs s64 — order-independent diff --git a/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr b/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr index 97dfb34..067988f 100644 --- a/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr +++ b/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr @@ -1,17 +1,35 @@ 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:21:15 + --> examples/1146-diagnostics-nonintegral-float-to-int.sx:29:16 | -21 | y : s64 = 1.5; // non-integral local initializer → 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:15:15 - | -15 | f : s64 = 3.5; // non-integral field default → error - | ^^^ +29 | 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:18:26 + --> examples/1146-diagnostics-nonintegral-float-to-int.sx:30:16 | -18 | badDefault :: (x : s64 = 2.5) -> s64 { return x; } // non-integral param default → error - | ^^^ +30 | ye : s64 = M + 0.5; // non-integral const-EXPRESSION 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 + | +21 | 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 + | +22 | fe : s64 = M + 0.5; // non-integral const-EXPRESSION 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 + | +25 | 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 + | +26 | badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral const-EXPR param default → error + | ^^^^^^^ diff --git a/issues/0095-typed-local-float-int-narrowing.md b/issues/0095-typed-local-float-int-narrowing.md index 68eda00..1d0a0c7 100644 --- a/issues/0095-typed-local-float-int-narrowing.md +++ b/issues/0095-typed-local-float-int-narrowing.md @@ -9,7 +9,7 @@ > dim — all reusing the single `program_index.floatToIntExact` / > `evalConstIntExpr` facility (no second integral check). > -> Fix (`src/ir/lower.zig`, `src/ir/module.zig`): +> Fix (`src/ir/lower.zig`, `src/ir/module.zig`, `src/ir/program_index.zig`): > - `Builder.constFloatInfo` reads a compile-time `const_float` back from its > Ref (value + span). > - `coerceToType` now means IMPLICIT coercion: its `.float_to_int` arm folds an @@ -24,11 +24,33 @@ > / `constExprValue` / `globalInitValue` fold an integral float to its int and > reject a non-integral one. > +> **Completion (F0.11 attempt 2)** — the direct-`const_float` coerce arm only +> caught a float LITERAL; a non-integral const-folded float EXPRESSION +> (`local/field/param : s64 = M + 0.5`) still truncated silently. Closed by: +> - New `program_index.evalConstFloatExpr` — the f64 counterpart to +> `evalConstIntExpr`, delegating every integer subtree back to it (no parallel +> integer logic), adding only the float literal / negate / `+ - * /` arms. +> - `Lowering.foldComptimeFloatInit` routes the typed LOCAL, struct FIELD +> default, and call ARGUMENT (incl. an expanded param default) through +> `evalConstFloatExpr` + `floatToIntExact`: an integral comptime float folds, +> a non-integral one errors, a genuine runtime float / `xx` cast is left to the +> normal path. (Run pure `evalConstFloatExpr` FIRST so a `$pack[i]` arg isn't +> spuriously type-resolved out of binding.) +> - One `Lowering.diagNonIntegralNarrow` now emits the narrowing wording at all +> five sites (coerce arm, global init, const-expr value, the typed-binding +> sites, and the typed-const path), so the typed-CONST non-integral diagnostic +> reads `cannot implicitly narrow non-integral float …` instead of the stale +> `initializer is a float literal / floating-point expression`. +> > Regression tests: `examples/0168-types-integral-float-to-int.sx` (positive — -> local/field/param/const fold, `xx`/`cast` truncate), `examples/1146-diagnostics- -> nonintegral-float-to-int.sx` (negative — local/param/field error), plus the -> integral-float const cases added to `examples/0162-types-typed-module-const- -> roundtrip.sx`. Non-integral const cases in `examples/1143` stay errors. +> 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 +> `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". ## Symptom A typed LOCAL (and likely typed param/field) silently truncates a floating-point diff --git a/readme.md b/readme.md index d0e3c3b..b16cc0d 100644 --- a/readme.md +++ b/readme.md @@ -125,13 +125,15 @@ while `F : f64 : M + 0.5` folds to `2.5`. **Float → integer narrowing (unified rule).** A float flowing into an integer-typed binding *without* a cast follows the same integral-fold rule an -array dimension uses: an **integral** float folds to its integer, a -**non-integral** float is a compile error. This is uniform across a typed local, -a parameter default, a struct field default, and a typed constant — -`y : s64 = 4.0` and `K : s64 : 4.0` both give `4` (and `K : s64 : M + 2.0` folds -to `4`), while `y : s64 = 1.5` and `N : s64 : 1.5` both error. An explicit -`xx` / `cast(s64)` is the escape hatch and always truncates (`y : s64 = xx 1.5` -→ `1`). +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. 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 d66c2ce..3765425 100644 --- a/specs.md +++ b/specs.md @@ -1426,14 +1426,19 @@ isReady : ValueListenable(bool) = map( an integer-typed binding without `xx`/`cast` is governed by the SAME rule an array dimension / lane count uses (see "Array dimensions are integral", §2): -- An **integral** compile-time float **folds** to its integer: - `y : s64 = 4.0` ≡ `y : s64 = 4`, `n : s64 = -2.0` ≡ `-2`. -- A **non-integral** compile-time float is a **compile error**: - `y : s64 = 1.5` → "cannot implicitly narrow non-integral float '1.5' to 's64'". +- 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`). +- 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 → + "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 - struct **field default**, and a typed module **constant** - (`K : s64 : 4.0` → 4; `N : s64 : 1.5` → error; `K : s64 : M + 2.0` → 4 when - the expression folds to an integer). + struct **field default**, a call **argument**, and a typed module **constant** + (`K : s64 : 4.0` → 4; `K : s64 : M + 2.0` → 4; `N : s64 : 1.5` and + `N : s64 : M + 0.5` → error). A **runtime** float (one with no compile-time + value) is unaffected — narrow it explicitly with `xx`/`cast`. **Explicit (narrowing)** — requires `xx` prefix (or `cast(T)`): - Integer to narrower integer (`s32` → `u8`) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 111b938..36928af 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -943,6 +943,18 @@ pub const Lowering = struct { // const → a bogus pointer that segfaults at the use site) and let the // count path fold it (`[N]s64` → 4). Issue 0088. if (!self.typedConstInitFits(cd.value, ty)) { + // A non-integral compile-time float into an integer const is the + // same implicit-narrowing failure as a typed local/field/param — + // report it with the unified wording (integral floats now FOLD here, + // so the old generic "initializer is a float literal/expression" + // message is stale). Every other mismatch keeps the generic wording. + if (self.isIntEx(ty) and isFloat(self.inferExprType(cd.value))) { + if (program_index_mod.evalConstFloatExpr(cd.value, self)) |fv| { + self.diagNonIntegralNarrow(cd.value.span, fv, ty); + _ = self.program_index.module_const_map.remove(cd.name); + return; + } + } if (self.diagnostics) |d| { d.addFmt(.err, cd.value.span, "type mismatch: constant '{s}' is declared '{s}' but its initializer is {s}", .{ cd.name, self.formatTypeName(ty), self.initializerDescription(cd.value), @@ -1090,8 +1102,7 @@ pub const Lowering = struct { .float_literal => |fl| blk: { if (self.isIntEx(var_ty)) { if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv }; - if (self.diagnostics) |d| - d.addFmt(.err, v.span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ fl.value, self.formatTypeName(var_ty) }); + self.diagNonIntegralNarrow(v.span, fl.value, var_ty); break :blk null; } break :blk inst_mod.ConstantValue{ .float = fl.value }; @@ -1208,8 +1219,7 @@ pub const Lowering = struct { .float_literal => |fl| blk: { if (self.isIntEx(expected_ty)) { if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv }; - if (self.diagnostics) |d| - d.addFmt(.err, expr.span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ fl.value, self.formatTypeName(expected_ty) }); + self.diagNonIntegralNarrow(expr.span, fl.value, expected_ty); break :blk null; } break :blk inst_mod.ConstantValue{ .float = fl.value }; @@ -2044,6 +2054,17 @@ pub const Lowering = struct { return; } } + // A compile-time float initializer narrowing into an integer + // local follows the unified rule (integral folds, non-integral + // errors); a runtime float / `xx` cast falls through to the + // normal lower+coerce below. + if (self.foldComptimeFloatInit(val, ty)) |folded| { + self.builder.store(slot, folded); + if (self.scope) |scope| { + scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); + } + return; + } const saved_target = self.target_type; const saved_fbv = self.force_block_value; self.target_type = ty; @@ -4649,16 +4670,11 @@ pub const Lowering = struct { // Field not specified — use default if available, else zero if (fi < field_defaults.len) { if (field_defaults[fi]) |default_expr| { - const saved_tt = self.target_type; - self.target_type = sf.ty; - const raw = self.lowerExpr(default_expr); - self.target_type = saved_tt; // Coerce the default to the field type at the IR // level (the implicit narrowing rule) so a float // default folds/errors here instead of being // silently bit-coerced by the backend. - const val = self.coerceToType(raw, self.builder.getRefType(raw), sf.ty); - fields.append(self.alloc, val) catch unreachable; + fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; } else { fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; } @@ -4694,12 +4710,7 @@ pub const Lowering = struct { for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| { if (fi < field_defaults.len) { if (field_defaults[fi]) |default_expr| { - const saved_tt = self.target_type; - self.target_type = sf.ty; - const raw = self.lowerExpr(default_expr); - self.target_type = saved_tt; - const val = self.coerceToType(raw, self.builder.getRefType(raw), sf.ty); - fields.append(self.alloc, val) catch unreachable; + fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; continue; } } @@ -7030,6 +7041,17 @@ pub const Lowering = struct { if (enum_payload_ty) |ept| { if (ai == 0) self.target_type = ept; } + // Implicit float→int narrowing of a compile-time float argument + // (incl. an expanded `param: T = expr` default) follows the unified + // rule: an integral comptime float folds, a non-integral one errors. + // A runtime float / `xx` cast is unaffected and coerces as before. + if (ai < param_types.len) { + if (self.foldComptimeFloatInit(arg, param_types[ai])) |folded| { + args.append(self.alloc, folded) catch unreachable; + self.target_type = saved_target; + continue; + } + } // Implicit address-of: when param expects *T and arg is an identifier // with an alloca of type T, pass the alloca pointer directly (reference // semantics, so mutations through the pointer are visible to the caller). @@ -14401,12 +14423,7 @@ pub const Lowering = struct { for (fields, 0..) |f, i| { if (i < field_defaults.len) { if (field_defaults[i]) |default_expr| { - const saved_tt = self.target_type; - self.target_type = f.ty; - const raw = self.lowerExpr(default_expr); - self.target_type = saved_tt; - const val = self.coerceToType(raw, self.builder.getRefType(raw), f.ty); - field_vals.append(self.alloc, val) catch unreachable; + field_vals.append(self.alloc, self.lowerCoercedDefault(default_expr, f.ty)) catch unreachable; } else { field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; } @@ -14551,6 +14568,65 @@ pub const Lowering = struct { return self.emitPlaceholder(field); } + /// Emit the unified non-integral float→int narrowing diagnostic (F0.11 / + /// issue 0095). ONE wording, ONE place: every site that rejects an implicit + /// narrowing of a non-integral compile-time float to an integer type calls + /// this, so the message + fix-it stay identical across the typed-binding + /// coerce arm, the field/param-default sites, the typed-const path, and the + /// global-initializer path. + fn diagNonIntegralNarrow(self: *Lowering, span: ast.Span, value: f64, dst_ty: TypeId) void { + if (self.diagnostics) |d| + d.addFmt(.err, span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ value, self.formatTypeName(dst_ty) }); + } + + /// Apply the unified float→int narrowing rule to a typed-binding initializer + /// EXPRESSION `node` whose declared type is `dst` (a typed local, a struct + /// field default, or a call argument incl. an expanded param default). When + /// `node` is a COMPILE-TIME float narrowing into an integer type: + /// - an INTEGRAL value (`4.0`, `M + 2.0`) folds to its `constInt`; + /// - a NON-integral value (`1.5`, `M + 0.5`) emits the narrowing + /// diagnostic and returns a placeholder so lowering finishes. + /// Returns null — so the caller lowers `node` normally — when the rule does + /// not apply: `dst` is not an integer, `node` is not statically float-typed, + /// or `node` is not a compile-time constant (a genuine runtime float keeps + /// truncating, and `xx` / `cast` keep their explicit-truncation escape since + /// a cast node's inferred type is the destination integer, not a float). + /// Reuses `program_index.evalConstIntExpr` (exact integral fold) + + /// `evalConstFloatExpr` (non-integral detection) + `floatToIntExact`. + fn foldComptimeFloatInit(self: *Lowering, node: *const Node, dst: TypeId) ?Ref { + if (!self.isIntEx(dst)) return null; + // PURE & side-effect-free, so it runs FIRST: a runtime / non-comptime / + // non-numeric node — incl. a `$pack[i]` index expression — folds to null + // and is left to the normal path untouched. (Calling `inferExprType` on + // a pack-index value before this guard would spuriously resolve the + // enclosing pack type outside an active binding.) + const fv = program_index_mod.evalConstFloatExpr(node, self) orelse return null; + // Only a FLOAT-flavored initializer narrows here; a plain comptime int + // (`5`, `M + 2`) is left to the normal integer path. Safe to infer now — + // `evalConstFloatExpr` only succeeds for literal / const-arithmetic + // nodes, never an unbound pack index. + if (!isFloat(self.inferExprType(node))) return null; + // Integral comptime float folds to its int (`floatToIntExact`, the same + // facility the array-dim / `$K: Count` paths use); a non-integral one is + // the narrowing error. + if (program_index_mod.floatToIntExact(fv)) |iv| return self.builder.constInt(iv, dst); + self.diagNonIntegralNarrow(node.span, fv, dst); + return self.builder.constInt(0, dst); + } + + /// Lower a struct field default `default_expr`, coerced to the field type + /// `field_ty`. A compile-time float default narrowing into an integer field + /// follows the unified rule via `foldComptimeFloatInit`; everything else + /// lowers under the field type as target and coerces at the IR level. + fn lowerCoercedDefault(self: *Lowering, default_expr: *const Node, field_ty: TypeId) Ref { + if (self.foldComptimeFloatInit(default_expr, field_ty)) |folded| return folded; + const saved_tt = self.target_type; + self.target_type = field_ty; + const raw = self.lowerExpr(default_expr); + self.target_type = saved_tt; + return self.coerceToType(raw, self.builder.getRefType(raw), field_ty); + } + /// How a float→int conversion is treated. An IMPLICIT coercion (a typed /// binding initializer) folds an integral compile-time float to its int and /// REJECTS a non-integral one; an EXPLICIT `xx` / `cast` always truncates. @@ -14657,12 +14733,10 @@ pub const Lowering = struct { if (program_index_mod.floatToIntExact(info.value)) |iv| { return self.builder.constInt(iv, dst_ty); } - if (self.diagnostics) |d| { - const sp = ast.Span{ .start = info.span.start, .end = info.span.end }; - d.addFmt(.err, sp, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ info.value, self.formatTypeName(dst_ty) }); - } - // Error already emitted; emit the truncating op so - // lowering finishes and `hasErrors()` aborts the build. + // Non-integral: diagnose, then fall through to the + // truncating op below so lowering finishes and + // `hasErrors()` aborts the build. + self.diagNonIntegralNarrow(.{ .start = info.span.start, .end = info.span.end }, info.value, dst_ty); } } return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index 21916f8..519108f 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -315,3 +315,43 @@ test "evalConstIntExpr folds an integral float literal, halts on a fractional on try std.testing.expectEqual(@as(?i64, 5), eval(&add, ctx)); try std.testing.expect(eval(&addbad, ctx) == null); } + +test "evalConstFloatExpr folds comptime float expressions, halts on runtime leaves" { + const eval = pi.evalConstFloatExpr; + const ctx = DimCtx{}; // M = 4, N = 6 + + var half = nFloat(0.5); + var two_f = nFloat(2.0); + var m = nIdent("M"); + var z = nIdent("Z"); // unbound — genuinely runtime + + // Leaves: a float literal is itself; an int leaf delegates to the int folder + // and promotes (`M` → 4.0); an unbound name is not a compile-time float. + try std.testing.expectEqual(@as(?f64, 0.5), eval(&half, ctx)); + try std.testing.expectEqual(@as(?f64, 4.0), eval(&m, ctx)); + try std.testing.expect(eval(&z, ctx) == null); + + // Mixed int+float arithmetic promotes to f64, order-independent + // (`M + 0.5` and `0.5 + M` → 4.5). `M + 2.0` is integral (6.0) but still a + // float value here — `floatToIntExact` is what the narrowing rule applies. + var mp = nBin(.add, &m, &half); + var pm = nBin(.add, &half, &m); + var mi = nBin(.add, &m, &two_f); + try std.testing.expectEqual(@as(?f64, 4.5), eval(&mp, ctx)); + try std.testing.expectEqual(@as(?f64, 4.5), eval(&pm, ctx)); + try std.testing.expectEqual(@as(?f64, 6.0), eval(&mi, ctx)); + + // Unary negate of a float expression. + var neg = nNeg(&mp); + try std.testing.expectEqual(@as(?f64, -4.5), eval(&neg, 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); + var cmp = nBin(.lt, &m, &half); + var zero_f = nFloat(0.0); + var divz = nBin(.div, &half, &zero_f); + try std.testing.expect(eval(&zp, ctx) == null); + try std.testing.expect(eval(&cmp, ctx) == null); + try std.testing.expect(eval(&divz, ctx) == null); +} diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index b7a6657..bd6fed9 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -218,6 +218,48 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 { }; } +/// Compile-time FLOAT value of a numeric expression, or null when it is not a +/// compile-time constant (some leaf is a runtime value) or is not a numeric +/// shape. THE float counterpart to `evalConstIntExpr`, used by the unified +/// float→int narrowing rule to (1) tell a compile-time float initializer apart +/// from a runtime one and (2) recover its value for `floatToIntExact` (integral +/// → fold) / the non-integral diagnostic. +/// +/// 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. +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 + // and `.min`/`.max` resolution in one place. + if (evalConstIntExpr(node, ctx)) |iv| return @floatFromInt(iv); + return switch (node.data) { + .float_literal => |lit| lit.value, + .unary_op => |u| switch (u.op) { + .negate => { + const v = evalConstFloatExpr(u.operand, ctx) orelse return null; + return -v; + }, + else => null, + }, + .binary_op => |b| { + const l = evalConstFloatExpr(b.lhs, ctx) orelse return null; + const r = evalConstFloatExpr(b.rhs, ctx) orelse return null; + return switch (b.op) { + .add => l + r, + .sub => l - r, + .mul => l * r, + .div => if (r == 0.0) null else l / r, + else => null, + }; + }, + else => null, + }; +} + /// The outcome of folding a comptime-int and narrowing it to a `u32` count /// (array dimension / Vector lane / value-param count). `foldDimU32` is the /// SINGLE place a folded integer becomes a `u32`, so the i64→u32 narrowing is