fix(ir): unify float→int narrowing — integral folds, non-integral errors [F0.11]

Issue 0095: a typed local/param/field silently TRUNCATED a float initializer
to an integer annotation (`y : s64 = 1.5` → 1) with no diagnostic. Agra ruled
the UNIFIED rule (Option B): an implicit float→int in a typed binding behaves
like the array-dimension rule —

  - an INTEGRAL compile-time float FOLDS to its int (`4.0` → 4, `-2.0` → -2);
  - a NON-integral float is a COMPILE ERROR (`1.5`, `4.5`);
  - explicit `xx` / `cast(T)` ALWAYS truncates (the escape hatch).

Applied consistently to typed local / param-default / field-default, typed
module CONST, and array dim — all reusing the single
`program_index.floatToIntExact` / `evalConstIntExpr` facility (no second
integral check).

- `Builder.constFloatInfo` reads a compile-time `const_float` back from its
  Ref (value + span).
- `coerceToType` is now the IMPLICIT path: its `.float_to_int` arm folds an
  integral const-float to `constInt`, else emits the narrowing diagnostic.
  `coerceExplicit` is the raw truncating path; `xx` (lowerXX) and `cast(T)`
  route through it so the escape still truncates.
- Field-default lowering (struct-literal pad, named-field default,
  buildDefaultValue) now coerces the default to the field type at the IR level
  (was silently bit-coerced by emitStructInit).
- Const path: `typedConstInitFits` accepts an integral float (literal or a
  `M + 2.0`-style expression folding via `evalComptimeInt`); `emitModuleConst`
  / `constExprValue` / `globalInitValue` fold an integral float to its int and
  reject a non-integral one — relaxing F0.7's blanket float rejection.

Tests: examples/0168 (positive: local/field/param/const fold, xx/cast
truncate), examples/1146 (negative: local/param/field error), integral-float
const cases added to examples/0162; non-integral const cases in 1143 stay
errors. specs.md + readme.md document the unified rule, cross-referencing the
array-dim rule. issues/0095 marked RESOLVED.
This commit is contained in:
agra
2026-06-05 15:34:33 +03:00
parent 341b62c197
commit 4c12e1de38
16 changed files with 362 additions and 14 deletions

View File

@@ -8,6 +8,9 @@
// - integer EXPRESSION → integer (`KE : s64 : M + 2`) — usable as a count too
// - integer EXPRESSION → float (`WE : f32 : M + 2`)
// - MIXED int+float EXPRESSION → float (`MF : f64 : M + 0.5`, both operand orders)
// - INTEGRAL float literal → integer (`KF : s64 : 4.0` → 4) — folds under the
// unified narrowing rule (F0.11), usable as a count too
// - INTEGRAL float EXPRESSION → integer (`KFE : s64 : M + 2.0` → 4)
//
// Companion to the negative example 1143: the issue-0088 fix rejects a typed
// const whose initializer mismatches its annotation, and these correctly-typed
@@ -31,6 +34,8 @@ KE : s64 : M + 2;
WE : f32 : M + 2;
MF : f64 : M + 0.5;
MFR : f64 : 0.5 + M;
KF : s64 : 4.0; // integral float literal → folds to 4
KFE : s64 : M + 2.0; // integral float expression → folds to 4
main :: () {
// Integer const: prints AND drives an array dimension (len 4).
@@ -55,4 +60,9 @@ main :: () {
// Mixed int+float const-EXPRESSION folds to the promoted float (2.5),
// operand-order-independent.
print("MF={} MFR={}\n", MF, MFR);
// Integral float const (literal + expression): folds to its integer under
// the unified narrowing rule; `KF` also drives an array dimension (len 4).
cc : [KF]s64 = ---;
print("KF={} len={} KFE={}\n", KF, cc.len, KFE);
}

View File

@@ -0,0 +1,46 @@
// Unified float→int narrowing rule (F0.11), POSITIVE side: an INTEGRAL float
// 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.
//
// 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.
#import "modules/std.sx";
Box :: struct {
n : s64 = 4.0; // integral float field default → folds to 4
}
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.
z : s64 = 4.0;
print("local={}\n", z);
// Negative integral float folds to its (negative) integer.
neg : s64 = -2.0;
print("neg={}\n", neg);
// Struct field default folds.
b := Box.{};
print("field={}\n", b.n);
// Param default folds.
print("param={}\n", withDefault());
// Module const folds (and can drive an array dimension: len 8).
a : [K]s64 = ---;
print("const={} len={}\n", K, a.len);
// Explicit escape: `xx` / `cast` always truncate, integral or not.
e : s64 = xx 4.9;
c : s64 = cast(s64) 1.5;
print("xx={} cast={}\n", e, c);
}

View File

@@ -0,0 +1,26 @@
// Unified float→int narrowing rule (F0.11), NEGATIVE side: a NON-INTEGRAL float
// 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.
//
// 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.
#import "modules/std.sx";
Bad :: struct {
f : s64 = 3.5; // non-integral field default → error
}
badDefault :: (x : s64 = 2.5) -> s64 { return x; } // non-integral param default → error
main :: () {
y : s64 = 1.5; // non-integral local initializer → error
b := Bad.{};
print("{}\n", b.f);
print("{}\n", badDefault());
print("{}\n", y);
}

View File

@@ -4,3 +4,4 @@ S=hi
P_is_null=true
KE=4 len=4 WE=4.000000
MF=2.500000 MFR=2.500000
KF=4 len=4 KFE=4

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

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

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,17 @@
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
|
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
| ^^^
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
|
18 | badDefault :: (x : s64 = 2.5) -> s64 { return x; } // non-integral param default → error
| ^^^