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 → integer (`KE : s64 : M + 2`) — usable as a count too
// - integer EXPRESSION → float (`WE : f32 : M + 2`) // - integer EXPRESSION → float (`WE : f32 : M + 2`)
// - MIXED int+float EXPRESSION → float (`MF : f64 : M + 0.5`, both operand orders) // - 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 // Companion to the negative example 1143: the issue-0088 fix rejects a typed
// const whose initializer mismatches its annotation, and these correctly-typed // const whose initializer mismatches its annotation, and these correctly-typed
@@ -31,6 +34,8 @@ KE : s64 : M + 2;
WE : f32 : M + 2; WE : f32 : M + 2;
MF : f64 : M + 0.5; MF : f64 : M + 0.5;
MFR : f64 : 0.5 + M; 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 :: () { main :: () {
// Integer const: prints AND drives an array dimension (len 4). // 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), // Mixed int+float const-EXPRESSION folds to the promoted float (2.5),
// operand-order-independent. // operand-order-independent.
print("MF={} MFR={}\n", MF, MFR); 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 P_is_null=true
KE=4 len=4 WE=4.000000 KE=4 len=4 WE=4.000000
MF=2.500000 MFR=2.500000 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
| ^^^

View File

@@ -0,0 +1,67 @@
# 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`):
> - `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.
>
> 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.
## 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.

View File

@@ -123,6 +123,16 @@ int+float arithmetic promotes to the float in either operand order (`n + 0.5` an
`0.5 + n` are both `f64`), so `C : s64 : M + 0.5` is rejected regardless of order `0.5 + n` are both `f64`), so `C : s64 : M + 0.5` is rejected regardless of order
while `F : f64 : M + 0.5` folds to `2.5`. 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`).
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**
site — a value binding (`:=` / typed local / parameter), a `::` constant or site — a value binding (`:=` / typed local / parameter), a `::` constant or

View File

@@ -893,6 +893,8 @@ A **count** is a compile-time integer used as an array dimension, a `Vector`
lane count, or a generic value-param count. Every count must be **integral**: an lane count, or a generic value-param count. Every count must be **integral**: an
integral float (`4.0`, or a float-typed const `N : f64 : 4.0`) folds to its integral float (`4.0`, or a float-typed const `N : f64 : 4.0`) folds to its
integer (`[4.0]s64` ≡ `[4]s64`), while a non-integral float (`4.5`) is rejected. integer (`[4.0]s64` ≡ `[4]s64`), while a non-integral float (`4.5`) is rejected.
This is the same integral-float rule a typed binding's float→integer initializer
follows (see "Implicit float → integer", §2 Type Conversions).
The accepted *range* of a count is **context-dependent** — zero is legal for The accepted *range* of a count is **context-dependent** — zero is legal for
some counts and not others: some counts and not others:
@@ -1418,13 +1420,27 @@ isReady : ValueListenable(bool) = map(
- Unsigned to strictly wider signed (`u8` → `s16`) - Unsigned to strictly wider signed (`u8` → `s16`)
- Any integer to any float (`u8` → `f32`, `s32` → `f64`) - Any integer to any float (`u8` → `f32`, `s32` → `f64`)
- Float to wider float (`f32` → `f64`) - Float to wider float (`f32` → `f64`)
- Integer and float literals can convert to any numeric type implicitly - Integer literals can convert to any numeric type implicitly
**Explicit (narrowing)** — requires `xx` prefix: **Implicit float → integer (the unified narrowing rule)** — a float flowing into
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'".
- 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).
**Explicit (narrowing)** — requires `xx` prefix (or `cast(T)`):
- Integer to narrower integer (`s32` → `u8`) - Integer to narrower integer (`s32` → `u8`)
- Signed to unsigned (`s32` → `u32`) - Signed to unsigned (`s32` → `u32`)
- Float to narrower float (`f64` → `f32`) - Float to narrower float (`f64` → `f32`)
- Float to any integer (`f64` → `u16`) - Float to any integer (`f64` → `u16`) — always **truncates**, integral or not
(`y : s64 = xx 1.5` → 1); this is the escape hatch from the implicit rule above
- Unsigned to signed of same or narrower width (`u8` → `s8`) - Unsigned to signed of same or narrower width (`u8` → `s8`)
The `xx` prefix operator marks an expression for auto-conversion to the expected type from context (assignment, declaration, argument, return): The `xx` prefix operator marks an expression for auto-conversion to the expected type from context (assignment, declaration, argument, return):

View File

@@ -975,6 +975,20 @@ pub const Lowering = struct {
/// `classify(.bool, s64)` yields `.widen` and would accept the bogus /// `classify(.bool, s64)` yields `.widen` and would accept the bogus
/// `B : s64 : true`. /// `B : s64 : true`.
fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool { fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
// An INTEGER-annotated constant accepts a compile-time INTEGRAL float —
// a literal (`K : s64 : 4.0`) or an expression that folds to an integer
// (`K : s64 : M + 2.0` → 4) — via the SAME `evalConstIntExpr` /
// `floatToIntExact` the array-dim path uses. A non-integral float
// (`1.5`, `M + 0.5`) folds to null and falls through to the rejecting
// checks below, matching the typed-local rule.
if (self.isIntEx(dst_ty)) {
switch (value.data) {
.float_literal, .binary_op, .unary_op => {
if (self.evalComptimeInt(value) != null) return true;
},
else => {},
}
}
return switch (value.data) { return switch (value.data) {
// `---` zero-inits at any type. // `---` zero-inits at any type.
.undef_literal => true, .undef_literal => true,
@@ -1071,7 +1085,17 @@ pub const Lowering = struct {
.null_literal => .null_val, .null_literal => .null_val,
.int_literal => |il| .{ .int = il.value }, .int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value }, .bool_literal => |bl| .{ .boolean = bl.value },
.float_literal => |fl| .{ .float = fl.value }, // A float initializer at an integer-typed global follows the
// implicit narrowing rule (integral folds, non-integral errors).
.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) });
break :blk null;
}
break :blk inst_mod.ConstantValue{ .float = fl.value };
},
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v), .array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v), .struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
@@ -1178,7 +1202,18 @@ pub const Lowering = struct {
return switch (expr.data) { return switch (expr.data) {
.int_literal => |il| .{ .int = il.value }, .int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value }, .bool_literal => |bl| .{ .boolean = bl.value },
.float_literal => |fl| .{ .float = fl.value }, // A float into an INTEGER destination follows the implicit
// narrowing rule: an integral float folds to its int, a
// non-integral one is a compile error (not a silent bit-coerce).
.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) });
break :blk null;
}
break :blk inst_mod.ConstantValue{ .float = fl.value };
},
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.undef_literal => .zeroinit, .undef_literal => .zeroinit,
// A `null` in a pointer (or optional-pointer) field is a // A `null` in a pointer (or optional-pointer) field is a
@@ -4616,8 +4651,13 @@ pub const Lowering = struct {
if (field_defaults[fi]) |default_expr| { if (field_defaults[fi]) |default_expr| {
const saved_tt = self.target_type; const saved_tt = self.target_type;
self.target_type = sf.ty; self.target_type = sf.ty;
const val = self.lowerExpr(default_expr); const raw = self.lowerExpr(default_expr);
self.target_type = saved_tt; 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, 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;
@@ -4656,8 +4696,9 @@ pub const Lowering = struct {
if (field_defaults[fi]) |default_expr| { if (field_defaults[fi]) |default_expr| {
const saved_tt = self.target_type; const saved_tt = self.target_type;
self.target_type = sf.ty; self.target_type = sf.ty;
const val = self.lowerExpr(default_expr); const raw = self.lowerExpr(default_expr);
self.target_type = saved_tt; 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, val) catch unreachable;
continue; continue;
} }
@@ -7112,7 +7153,7 @@ pub const Lowering = struct {
if (src_ty == .any) { if (src_ty == .any) {
return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty); return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty);
} }
return self.coerceToType(val, src_ty, dst_ty); return self.coerceExplicit(val, src_ty, dst_ty);
} }
// Runtime cast — fall through to builtin handling // Runtime cast — fall through to builtin handling
} }
@@ -13934,7 +13975,7 @@ pub const Lowering = struct {
.coerce => {}, .coerce => {},
} }
const result = self.coerceToType(operand, src_ty, dst_ty); const result = self.coerceExplicit(operand, src_ty, dst_ty);
// User-space fallback via `impl Into(Target) for Source`. Only fires // User-space fallback via `impl Into(Target) for Source`. Only fires
// when the target was explicitly named (not the .s64 default), src and // when the target was explicitly named (not the .s64 default), src and
@@ -14362,8 +14403,9 @@ pub const Lowering = struct {
if (field_defaults[i]) |default_expr| { if (field_defaults[i]) |default_expr| {
const saved_tt = self.target_type; const saved_tt = self.target_type;
self.target_type = f.ty; self.target_type = f.ty;
const val = self.lowerExpr(default_expr); const raw = self.lowerExpr(default_expr);
self.target_type = saved_tt; 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, 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;
@@ -14402,6 +14444,17 @@ pub const Lowering = struct {
} }
fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref { fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref {
// An integer-typed const whose initializer is a compile-time integer —
// an int literal/expression, OR an INTEGRAL float that `typedConstInitFits`
// accepted under the unified narrowing rule — materializes as its folded
// int through the SAME `evalConstIntExpr` the count / array-dim path uses.
// (`K : s64 : 4.0` → 4; `K : s64 : M + 2.0` → 4.) Non-foldable shapes
// fall through to the per-kind emitters below.
if (self.isIntEx(ci.ty)) {
if (self.evalComptimeInt(ci.value)) |iv| {
return self.builder.constInt(iv, ci.ty);
}
}
switch (ci.value.data) { switch (ci.value.data) {
.int_literal => |lit| { .int_literal => |lit| {
// If declared type is float, convert integer value to float constant // If declared type is float, convert integer value to float constant
@@ -14498,9 +14551,26 @@ pub const Lowering = struct {
return self.emitPlaceholder(field); return self.emitPlaceholder(field);
} }
/// 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.
const CoerceMode = enum { implicit, explicit };
/// Insert a conversion if src_ty and dst_ty differ. /// Insert a conversion if src_ty and dst_ty differ.
/// Handles int widening/narrowing, float widening/narrowing, and int↔float. /// Handles int widening/narrowing, float widening/narrowing, and int↔float.
/// IMPLICIT coercion — the typed-binding initializer path. A compile-time
/// float narrowing to an integer folds when integral, errors when not.
fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
return self.coerceMode(val, src_ty, dst_ty, .implicit);
}
/// EXPLICIT coercion — the `xx` / `cast(T)` escape hatch. A float→int here
/// always truncates, bypassing the integral-fold / non-integral-error rule.
fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
return self.coerceMode(val, src_ty, dst_ty, .explicit);
}
fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mode: CoerceMode) Ref {
// PLANNING: classify the built-in coercion (conversions.zig). // PLANNING: classify the built-in coercion (conversions.zig).
// EMISSION: each arm below reproduces the original lowering. // EMISSION: each arm below reproduces the original lowering.
switch (self.coercionResolver().classify(src_ty, dst_ty)) { switch (self.coercionResolver().classify(src_ty, dst_ty)) {
@@ -14534,7 +14604,7 @@ pub const Lowering = struct {
defer elems.deinit(self.alloc); defer elems.deinit(self.alloc);
for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| { for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| {
const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf); const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf);
elems.append(self.alloc, self.coerceToType(fv, sf, df)) catch unreachable; elems.append(self.alloc, self.coerceMode(fv, sf, df, mode)) catch unreachable;
} }
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty); return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty);
}, },
@@ -14542,14 +14612,14 @@ pub const Lowering = struct {
.optional_unwrap => { .optional_unwrap => {
const child_ty = self.module.types.get(src_ty).optional.child; const child_ty = self.module.types.get(src_ty).optional.child;
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
return self.coerceToType(unwrapped, child_ty, dst_ty); return self.coerceMode(unwrapped, child_ty, dst_ty, mode);
}, },
// void → Optional: produce null (void is the type of null_literal) // void → Optional: produce null (void is the type of null_literal)
.void_to_optional => return self.builder.constNull(dst_ty), .void_to_optional => return self.builder.constNull(dst_ty),
// Concrete → Optional wrapping (coerce to the inner type first) // Concrete → Optional wrapping (coerce to the inner type first)
.optional_wrap => { .optional_wrap => {
const child_ty = self.module.types.get(dst_ty).optional.child; const child_ty = self.module.types.get(dst_ty).optional.child;
const coerced = self.coerceToType(val, src_ty, child_ty); const coerced = self.coerceMode(val, src_ty, child_ty, mode);
return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty); return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty);
}, },
// Concrete → Protocol (auto type erasure) // Concrete → Protocol (auto type erasure)
@@ -14575,7 +14645,28 @@ pub const Lowering = struct {
return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy); return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy);
}, },
.int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
.float_to_int => return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .float_to_int => {
// Implicit float→int narrowing follows the unified rule (the
// same `floatToIntExact` the array-dim / `$K: Count` paths use):
// a compile-time INTEGRAL float folds to its int, a NON-integral
// one is a compile error. Explicit `xx` / `cast` (mode
// `.explicit`) skips this and truncates. A runtime float has no
// compile-time value to fold — it truncates as before.
if (mode == .implicit) {
if (self.builder.constFloatInfo(val)) |info| {
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.
}
}
return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
},
// Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot. // Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot.
// Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches // Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches
// to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level // to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level

View File

@@ -114,3 +114,28 @@ test "Module: globals" {
try std.testing.expectEqual(GlobalId.fromIndex(0), id); try std.testing.expectEqual(GlobalId.fromIndex(0), id);
try std.testing.expectEqual(TypeId.s32, mod.globals.items[0].ty); try std.testing.expectEqual(TypeId.s32, mod.globals.items[0].ty);
} }
test "Builder.constFloatInfo reads a const_float back, null for non-floats" {
const alloc = std.testing.allocator;
var mod = Module.init(alloc);
defer mod.deinit();
var b = Builder.init(&mod);
const name = mod.types.internString("f");
const entry_name = mod.types.internString("entry");
_ = b.beginFunction(name, &.{}, .void);
const entry = b.appendBlock(entry_name, &.{});
b.switchToBlock(entry);
// A const_float reads back its value (the implicit float→int rule consults
// this to fold an integral literal / locate a non-integral one).
const fref = b.constFloat(4.0, .f64);
const info = b.constFloatInfo(fref) orelse return error.TestUnexpectedResult;
try std.testing.expectEqual(@as(f64, 4.0), info.value);
// A non-float instruction is not a const_float — null.
const iref = b.constInt(7, .s64);
try std.testing.expect(b.constFloatInfo(iref) == null);
b.finalize();
}

View File

@@ -247,6 +247,13 @@ pub const ImplTable = struct {
// ── Builder ───────────────────────────────────────────────────────────── // ── Builder ─────────────────────────────────────────────────────────────
// Fluent API for constructing one function at a time. // Fluent API for constructing one function at a time.
/// A `const_float` instruction read back from its Ref: the compile-time value
/// and the span it was emitted with.
pub const ConstFloatInfo = struct {
value: f64,
span: Span,
};
pub const Builder = struct { pub const Builder = struct {
module: *Module, module: *Module,
func: ?FuncId = null, func: ?FuncId = null,
@@ -347,6 +354,28 @@ pub const Builder = struct {
return .unresolved; return .unresolved;
} }
/// If `ref` points at a compile-time `const_float` instruction, return its
/// value and the span it was emitted with; else null. The implicit
/// float→int coercion rule reads this to fold an integral literal to its
/// int (and to locate a non-integral one for its diagnostic).
pub fn constFloatInfo(self: *Builder, ref: Ref) ?ConstFloatInfo {
if (self.func == null) return null;
const func = self.currentFunc();
const ref_idx = @intFromEnum(ref);
if (ref_idx < func.params.len) return null;
for (func.blocks.items) |*block| {
const first = block.first_ref;
if (ref_idx >= first and ref_idx < first + @as(u32, @intCast(block.insts.items.len))) {
const i = block.insts.items[ref_idx - first];
return switch (i.op) {
.const_float => |v| .{ .value = v, .span = i.span },
else => null,
};
}
}
return null;
}
// ── Emit helpers ──────────────────────────────────────────────── // ── Emit helpers ────────────────────────────────────────────────
pub fn emit(self: *Builder, op: Op, ty: TypeId) Ref { pub fn emit(self: *Builder, op: Op, ty: TypeId) Ref {