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:
113
src/ir/lower.zig
113
src/ir/lower.zig
@@ -975,6 +975,20 @@ pub const Lowering = struct {
|
||||
/// `classify(.bool, s64)` yields `.widen` and would accept the bogus
|
||||
/// `B : s64 : true`.
|
||||
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) {
|
||||
// `---` zero-inits at any type.
|
||||
.undef_literal => true,
|
||||
@@ -1071,7 +1085,17 @@ pub const Lowering = struct {
|
||||
.null_literal => .null_val,
|
||||
.int_literal => |il| .{ .int = il.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) },
|
||||
.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),
|
||||
@@ -1178,7 +1202,18 @@ pub const Lowering = struct {
|
||||
return switch (expr.data) {
|
||||
.int_literal => |il| .{ .int = il.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) },
|
||||
.undef_literal => .zeroinit,
|
||||
// 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| {
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = sf.ty;
|
||||
const val = self.lowerExpr(default_expr);
|
||||
const raw = self.lowerExpr(default_expr);
|
||||
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;
|
||||
} else {
|
||||
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
||||
@@ -4656,8 +4696,9 @@ pub const Lowering = struct {
|
||||
if (field_defaults[fi]) |default_expr| {
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = sf.ty;
|
||||
const val = self.lowerExpr(default_expr);
|
||||
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;
|
||||
}
|
||||
@@ -7112,7 +7153,7 @@ pub const Lowering = struct {
|
||||
if (src_ty == .any) {
|
||||
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
|
||||
}
|
||||
@@ -13934,7 +13975,7 @@ pub const Lowering = struct {
|
||||
.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
|
||||
// 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| {
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = f.ty;
|
||||
const val = self.lowerExpr(default_expr);
|
||||
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 {
|
||||
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 {
|
||||
// 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) {
|
||||
.int_literal => |lit| {
|
||||
// If declared type is float, convert integer value to float constant
|
||||
@@ -14498,9 +14551,26 @@ pub const Lowering = struct {
|
||||
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.
|
||||
/// 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 {
|
||||
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).
|
||||
// EMISSION: each arm below reproduces the original lowering.
|
||||
switch (self.coercionResolver().classify(src_ty, dst_ty)) {
|
||||
@@ -14534,7 +14604,7 @@ pub const Lowering = struct {
|
||||
defer elems.deinit(self.alloc);
|
||||
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);
|
||||
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);
|
||||
},
|
||||
@@ -14542,14 +14612,14 @@ pub const Lowering = struct {
|
||||
.optional_unwrap => {
|
||||
const child_ty = self.module.types.get(src_ty).optional.child;
|
||||
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_to_optional => return self.builder.constNull(dst_ty),
|
||||
// Concrete → Optional wrapping (coerce to the inner type first)
|
||||
.optional_wrap => {
|
||||
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);
|
||||
},
|
||||
// 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);
|
||||
},
|
||||
.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.
|
||||
// Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches
|
||||
// to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level
|
||||
|
||||
@@ -114,3 +114,28 @@ test "Module: globals" {
|
||||
try std.testing.expectEqual(GlobalId.fromIndex(0), id);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -247,6 +247,13 @@ pub const ImplTable = struct {
|
||||
// ── Builder ─────────────────────────────────────────────────────────────
|
||||
// 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 {
|
||||
module: *Module,
|
||||
func: ?FuncId = null,
|
||||
@@ -347,6 +354,28 @@ pub const Builder = struct {
|
||||
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 ────────────────────────────────────────────────
|
||||
|
||||
pub fn emit(self: *Builder, op: Op, ty: TypeId) Ref {
|
||||
|
||||
Reference in New Issue
Block a user