# 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`. > > 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 > `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 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.