# 0095 — typed local/decl silently truncates a float initializer to an integer annotation > **RESOLVED (F0.11).** Agra ruled the UNIFIED rule (Option B): an implicit > float→int in a typed binding behaves exactly like the array-dimension rule — > an **integral** float FOLDS to its integer (`4.0` → 4, `-2.0` → -2), a > **non-integral** float is a COMPILE ERROR (`1.5`, `4.5`), and an explicit > `xx` / `cast(T)` ALWAYS truncates (the escape). Applied consistently across > 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). > > 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 > 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 that folds via `evalComptimeInt`); `emitModuleConst` > / `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`. > > **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 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" > (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 initializer to an integer annotation instead of rejecting or requiring an explicit cast. Observed: - `y : s64 = 1.5;` → y == 1 (float literal truncated, no diagnostic) - `y : s64 = 2 + 0.5;` → y == 2 (float-valued expr truncated, no diagnostic) Expected: a type-mismatch / narrowing diagnostic (consistent with typed MODULE CONSTS, which after F0.7 reject `N : s64 : 1.5` and `N : s64 : M + 0.5`). Today consts are strict but locals are lenient — an inconsistency. ## Reproduction ```sx #import "modules/std.sx"; main :: () { y : s64 = 1.5; print("{}\n", y); // prints 1 } ``` ## Investigation prompt Decide + implement the language rule for implicit float→int narrowing in a TYPED binding (local / param / field) initializer. Module consts already reject it (F0.7, registerTypedModuleConst + typedConstInitFits/constExprInitFits). Make typed-local/param/field assignment-coercion consistent: either reject a non-integral float→int initializer with a diagnostic (matching the const path) or require an explicit `xx`/cast. Suspected area: the assignment / typed-binding coercion path (coerceToType ladder, specs.md §"coercion") in src/ir/lower.zig. Verify `y : s64 = 1.5` errors (or requires a cast); confirm integral-float folding rules (specs.md: `4.0`→4 ok, `4.5` rejected) stay consistent. Then gate. ## Disposition Discovered during F0.7 (issue 0088) attempt-2 review. Agra ruled F0.7 fixes the inferExprType ROOT for binary-op promotion; this typed-LOCAL narrowing is a SEPARATE assignment-coercion concern -> its own scheduled step.