fix(ir): narrow non-integral const-float EXPRESSIONS at typed local/field/param; align const message [F0.11]

Completes issue 0095 (attempt 2). The attempt-1 coerce arm only caught a direct
`const_float` literal, so a non-integral const-folded float EXPRESSION still
truncated silently at a typed local / field default / param default:

  M :: 2;
  local : s64 = M + 0.5;   // → 2  (silent truncation — BUG; now ERRORS)
  fld   : s64 = M + 0.5;   // field default — same
  take(x : s64 = M + 0.5)  // param default — same

while the typed-CONST site already errored. The integral expression
(`M + 2.0` → 4) folded but the runtime/explicit-cast paths must stay untouched.

Fix:
- New `program_index.evalConstFloatExpr` — the f64 counterpart to
  `evalConstIntExpr`, delegating every integer subtree back to it (no parallel
  integer logic) and adding only the float literal / unary-negate / `+ - * /`
  arms. Pure (no diagnostics, no resolution side effects).
- `Lowering.foldComptimeFloatInit` applies the unified rule to a typed-binding
  initializer EXPRESSION: an integral comptime float folds to its `constInt`, a
  non-integral one errors, a genuine runtime float / `xx`-cast falls through to
  the normal path. It runs `evalConstFloatExpr` FIRST (pure) so a `$pack[i]`
  argument is never spuriously type-resolved outside an active binding, then
  gates on `isFloat(inferExprType)` so a plain comptime int is left alone.
  Wired into the typed-local path, the three struct field-default sites (via a
  shared `lowerCoercedDefault`), and the call-argument loop (covers expanded
  param defaults).
- 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). The typed-CONST non-integral diagnostic
  therefore reads "cannot implicitly narrow non-integral float …" instead of
  the stale "initializer is a float literal / floating-point expression".

Tests: examples/1146 (negative) extended with non-integral const-EXPRESSION
cases at local/field/param; examples/0168 (positive) extended with integral
const-EXPRESSION folds and `xx (M + 0.5)` truncation; examples/1143 reconciled
to the aligned const message (G/BAD/BAD2 stay errors); unit test
`evalConstFloatExpr folds comptime float expressions`. Full gate green (447).
This commit is contained in:
agra
2026-06-05 16:28:12 +03:00
parent 4c12e1de38
commit 43d44fff75
11 changed files with 306 additions and 85 deletions

View File

@@ -2,17 +2,23 @@
// 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. The escape hatch (`xx` / `cast`) still TRUNCATES any float, // PARAM default. It folds whether written as a float LITERAL (`4.0`) or a
// integral or not. // 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). // 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; the rule now folds an // initializer (`y : s64 = 1.5` → 1) with no diagnostic, and a non-integral const
// integral float and rejects a non-integral one. // 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"; #import "modules/std.sx";
M :: 2; // module const, for the const-EXPRESSION 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
} }
withDefault :: (x : s64 = 6.0) -> s64 { return x; } // param default → 6 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 K : s64 : 8.0; // integral float module const → folds to 8
main :: () { main :: () {
// Typed local: integral float folds. // Typed local: integral float folds (literal + expression).
z : s64 = 4.0; 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. // 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 default folds. // Struct field defaults fold (literal + expression).
b := Box.{}; b := Box.{};
print("field={}\n", b.n); print("field={} fieldExpr={}\n", b.n, b.ne);
// Param default folds. // Param default folds.
print("param={}\n", withDefault()); print("param={}\n", withDefault());
@@ -39,8 +46,10 @@ main :: () {
a : [K]s64 = ---; a : [K]s64 = ---;
print("const={} len={}\n", K, a.len); 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; e : s64 = xx 4.9;
c : s64 = cast(s64) 1.5; 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);
} }

View File

@@ -2,25 +2,34 @@
// implicitly narrowing to an integer-typed binding is a COMPILE ERROR — not a // 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 // 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). The fix is the integral-fold / // at the offending float and aborts (exit 1). It fires whether the float is a
// non-integral-error rule shared with the array-dimension path. // 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`) // 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. // 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"; #import "modules/std.sx";
M :: 2; // module const, for the const-EXPRESSION cases
Bad :: struct { 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 :: () { 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.{}; b := Bad.{};
print("{}\n", b.f); print("{} {}\n", b.f, b.fe);
print("{}\n", badDefault()); print("{} {}\n", badLit(), badExpr());
print("{}\n", y); print("{} {}\n", y, ye);
} }

View File

@@ -1,6 +1,6 @@
local=4 local=4 localExpr=4
neg=-2 neg=-2
field=4 field=4 fieldExpr=4
param=6 param=6
const=8 len=8 const=8 len=8
xx=4 cast=1 xx=4 cast=1 xxExpr=2

View File

@@ -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 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 --> examples/1143-diagnostics-typed-module-const-mismatch.sx:27:15
| |
27 | G : s64 : 1.5; // float literal where an integer is annotated 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 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 --> 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 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 --> 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 31 | BAD2 : s64 : 0.5 + M; // mixed float+int (float LHS) → f64, rejected vs s64 — order-independent

View File

@@ -1,17 +1,35 @@
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:21:15 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:29:16
| |
21 | y : s64 = 1.5; // non-integral local initializer → error 29 | y : s64 = 1.5; // non-integral float LITERAL 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:15:15
|
15 | f : s64 = 3.5; // non-integral 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: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
| ^^^^^^^

View File

@@ -9,7 +9,7 @@
> dim — all reusing the single `program_index.floatToIntExact` / > dim — all reusing the single `program_index.floatToIntExact` /
> `evalConstIntExpr` facility (no second integral check). > `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 > - `Builder.constFloatInfo` reads a compile-time `const_float` back from its
> Ref (value + span). > Ref (value + span).
> - `coerceToType` now means IMPLICIT coercion: its `.float_to_int` arm folds an > - `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 > / `constExprValue` / `globalInitValue` fold an integral float to its int and
> reject a non-integral one. > 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 — > Regression tests: `examples/0168-types-integral-float-to-int.sx` (positive —
> local/field/param/const fold, `xx`/`cast` truncate), `examples/1146-diagnostics- > local/field/param/const fold, integral const-EXPRESSION (`M + 2.0`) folds,
> nonintegral-float-to-int.sx` (negative — local/param/field error), plus the > `xx`/`cast` truncate incl. `xx (M + 0.5)`), `examples/1146-diagnostics-
> integral-float const cases added to `examples/0162-types-typed-module-const- > nonintegral-float-to-int.sx` (negative — non-integral LITERAL and const-
> roundtrip.sx`. Non-integral const cases in `examples/1143` stay errors. > 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 ## 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

View File

@@ -125,13 +125,15 @@ while `F : f64 : M + 0.5` folds to `2.5`.
**Float → integer narrowing (unified rule).** A float flowing into an **Float → integer narrowing (unified rule).** A float flowing into an
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** float folds to its integer, a array dimension uses: an **integral** compile-time float folds to its integer, a
**non-integral** float is a compile error. This is uniform across a typed local, **non-integral** one is a compile error. It holds whether the value is a literal
a parameter default, a struct field default, and a typed constant — or a const expression, and is uniform across a typed local, a parameter default,
`y : s64 = 4.0` and `K : s64 : 4.0` both give `4` (and `K : s64 : M + 2.0` folds a struct field default, a call argument, and a typed constant — `y : s64 = 4.0`,
to `4`), while `y : s64 = 1.5` and `N : s64 : 1.5` both error. An explicit `K : s64 : 4.0`, and `y : s64 = M + 2.0` all give `4`, while `y : s64 = 1.5`,
`xx` / `cast(s64)` is the escape hatch and always truncates (`y : s64 = xx 1.5` `N : s64 : 1.5`, and `y : s64 = M + 0.5` all error (one wording everywhere:
→ `1`). `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**

View File

@@ -1426,14 +1426,19 @@ isReady : ValueListenable(bool) = map(
an integer-typed binding without `xx`/`cast` is governed by the SAME rule an 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): array dimension / lane count uses (see "Array dimensions are integral", §2):
- An **integral** compile-time float **folds** to its integer: - An **integral** compile-time float **folds** to its integer, whether written
`y : s64 = 4.0` ≡ `y : s64 = 4`, `n : s64 = -2.0` ≡ `-2`. as a literal or a const expression: `y : s64 = 4.0` ≡ `y : s64 = 4`,
- A **non-integral** compile-time float is a **compile error**: `n : s64 = -2.0` ≡ `-2`, `y : s64 = M + 2.0` → 4 (`M :: 2`).
`y : s64 = 1.5` → "cannot implicitly narrow non-integral float '1.5' to 's64'". - 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 - This applies uniformly to a typed **local**, a function **param default**, a
struct **field default**, and a typed module **constant** struct **field default**, a call **argument**, and a typed module **constant**
(`K : s64 : 4.0` → 4; `N : s64 : 1.5` → error; `K : s64 : M + 2.0` → 4 when (`K : s64 : 4.0` → 4; `K : s64 : M + 2.0` → 4; `N : s64 : 1.5` and
the expression folds to an integer). `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)`): **Explicit (narrowing)** — requires `xx` prefix (or `cast(T)`):
- Integer to narrower integer (`s32` → `u8`) - Integer to narrower integer (`s32` → `u8`)

View File

@@ -943,6 +943,18 @@ pub const Lowering = struct {
// const → a bogus pointer that segfaults at the use site) and let the // const → a bogus pointer that segfaults at the use site) and let the
// count path fold it (`[N]s64` → 4). Issue 0088. // count path fold it (`[N]s64` → 4). Issue 0088.
if (!self.typedConstInitFits(cd.value, ty)) { 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| { if (self.diagnostics) |d| {
d.addFmt(.err, cd.value.span, "type mismatch: constant '{s}' is declared '{s}' but its initializer is {s}", .{ 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), cd.name, self.formatTypeName(ty), self.initializerDescription(cd.value),
@@ -1090,8 +1102,7 @@ pub const Lowering = struct {
.float_literal => |fl| blk: { .float_literal => |fl| blk: {
if (self.isIntEx(var_ty)) { if (self.isIntEx(var_ty)) {
if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv }; if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
if (self.diagnostics) |d| self.diagNonIntegralNarrow(v.span, fl.value, var_ty);
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) });
break :blk null; break :blk null;
} }
break :blk inst_mod.ConstantValue{ .float = fl.value }; break :blk inst_mod.ConstantValue{ .float = fl.value };
@@ -1208,8 +1219,7 @@ pub const Lowering = struct {
.float_literal => |fl| blk: { .float_literal => |fl| blk: {
if (self.isIntEx(expected_ty)) { if (self.isIntEx(expected_ty)) {
if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv }; if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
if (self.diagnostics) |d| self.diagNonIntegralNarrow(expr.span, fl.value, expected_ty);
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) });
break :blk null; break :blk null;
} }
break :blk inst_mod.ConstantValue{ .float = fl.value }; break :blk inst_mod.ConstantValue{ .float = fl.value };
@@ -2044,6 +2054,17 @@ pub const Lowering = struct {
return; 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_target = self.target_type;
const saved_fbv = self.force_block_value; const saved_fbv = self.force_block_value;
self.target_type = ty; self.target_type = ty;
@@ -4649,16 +4670,11 @@ pub const Lowering = struct {
// Field not specified — use default if available, else zero // Field not specified — use default if available, else zero
if (fi < field_defaults.len) { if (fi < field_defaults.len) {
if (field_defaults[fi]) |default_expr| { 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 // Coerce the default to the field type at the IR
// level (the implicit narrowing rule) so a float // level (the implicit narrowing rule) so a float
// default folds/errors here instead of being // default folds/errors here instead of being
// silently bit-coerced by the backend. // silently bit-coerced by the backend.
const val = self.coerceToType(raw, self.builder.getRefType(raw), sf.ty); fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable;
fields.append(self.alloc, val) catch unreachable;
} else { } else {
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; 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| { for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| {
if (fi < field_defaults.len) { if (fi < field_defaults.len) {
if (field_defaults[fi]) |default_expr| { if (field_defaults[fi]) |default_expr| {
const saved_tt = self.target_type; fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable;
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;
continue; continue;
} }
} }
@@ -7030,6 +7041,17 @@ pub const Lowering = struct {
if (enum_payload_ty) |ept| { if (enum_payload_ty) |ept| {
if (ai == 0) self.target_type = 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 // Implicit address-of: when param expects *T and arg is an identifier
// with an alloca of type T, pass the alloca pointer directly (reference // with an alloca of type T, pass the alloca pointer directly (reference
// semantics, so mutations through the pointer are visible to the caller). // semantics, so mutations through the pointer are visible to the caller).
@@ -14401,12 +14423,7 @@ pub const Lowering = struct {
for (fields, 0..) |f, i| { for (fields, 0..) |f, i| {
if (i < field_defaults.len) { if (i < field_defaults.len) {
if (field_defaults[i]) |default_expr| { if (field_defaults[i]) |default_expr| {
const saved_tt = self.target_type; field_vals.append(self.alloc, self.lowerCoercedDefault(default_expr, f.ty)) catch unreachable;
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;
} else { } else {
field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable;
} }
@@ -14551,6 +14568,65 @@ pub const Lowering = struct {
return self.emitPlaceholder(field); 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 /// How a float→int conversion is treated. An IMPLICIT coercion (a typed
/// binding initializer) folds an integral compile-time float to its int and /// binding initializer) folds an integral compile-time float to its int and
/// REJECTS a non-integral one; an EXPLICIT `xx` / `cast` always truncates. /// 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| { if (program_index_mod.floatToIntExact(info.value)) |iv| {
return self.builder.constInt(iv, dst_ty); return self.builder.constInt(iv, dst_ty);
} }
if (self.diagnostics) |d| { // Non-integral: diagnose, then fall through to the
const sp = ast.Span{ .start = info.span.start, .end = info.span.end }; // truncating op below so lowering finishes and
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) }); // `hasErrors()` aborts the build.
} self.diagNonIntegralNarrow(.{ .start = info.span.start, .end = info.span.end }, info.value, dst_ty);
// Error already emitted; emit the truncating op so
// lowering finishes and `hasErrors()` aborts the build.
} }
} }
return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);

View File

@@ -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.expectEqual(@as(?i64, 5), eval(&add, ctx));
try std.testing.expect(eval(&addbad, ctx) == null); 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);
}

View File

@@ -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, `<IntType>.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 /// The outcome of folding a comptime-int and narrowing it to a `u32` count
/// (array dimension / Vector lane / value-param count). `foldDimU32` is the /// (array dimension / Vector lane / value-param count). `foldDimU32` is the
/// SINGLE place a folded integer becomes a `u32`, so the i64→u32 narrowing is /// SINGLE place a folded integer becomes a `u32`, so the i64→u32 narrowing is