From e664bf4d81e9266ee4749b5da88cab4554499bf6 Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 30 Jul 2025 23:22:32 +0100 Subject: [PATCH 1/2] Sema: compile error on lossy int to float coercion Resolves: #21586 --- src/Sema.zig | 41 ++++++++++++++----- .../int_to_float_coercion_loses_precision.zig | 9 ++++ 2 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 test/cases/compile_errors/int_to_float_coercion_loses_precision.zig diff --git a/src/Sema.zig b/src/Sema.zig index 94bf21e03b..edba4e6874 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -28733,17 +28733,36 @@ fn coerceExtra( break :int; }; const result_val = try val.floatFromIntAdvanced(sema.arena, inst_ty, dest_ty, pt, .sema); - // TODO implement this compile error - //const int_again_val = try result_val.intFromFloat(sema.arena, inst_ty); - //if (!int_again_val.eql(val, inst_ty, zcu)) { - // return sema.fail( - // block, - // inst_src, - // "type '{f}' cannot represent integer value '{f}'", - // .{ dest_ty.fmt(pt), val }, - // ); - //} - return Air.internedToRef(result_val.toIntern()); + const fits: bool = switch (ip.indexToKey(result_val.toIntern())) { + else => unreachable, + .undef => true, + .float => |float| fits: { + var buffer: InternPool.Key.Int.Storage.BigIntSpace = undefined; + const operand_big_int = val.toBigInt(&buffer, zcu); + switch (float.storage) { + inline else => |x| { + if (!std.math.isFinite(x)) break :fits false; + var result_big_int: std.math.big.int.Mutable = .{ + .limbs = try sema.arena.alloc(std.math.big.Limb, std.math.big.int.calcLimbLen(x)), + .len = undefined, + .positive = undefined, + }; + switch (result_big_int.setFloat(x, .nearest_even)) { + .inexact => break :fits false, + .exact => {}, + } + break :fits result_big_int.toConst().eql(operand_big_int); + }, + } + }, + }; + if (!fits) return sema.fail( + block, + inst_src, + "type '{f}' cannot represent integer value '{f}'", + .{ dest_ty.fmt(pt), val.fmtValue(pt) }, + ); + return .fromValue(result_val); }, else => {}, }, diff --git a/test/cases/compile_errors/int_to_float_coercion_loses_precision.zig b/test/cases/compile_errors/int_to_float_coercion_loses_precision.zig new file mode 100644 index 0000000000..bc1d8e7dee --- /dev/null +++ b/test/cases/compile_errors/int_to_float_coercion_loses_precision.zig @@ -0,0 +1,9 @@ +export fn foo() void { + const int: u16 = 65535; + const float: f16 = int; + _ = float; +} + +// error +// +// :3:24: error: type 'f16' cannot represent integer value '65535' From 64bf8bb146099b51d74635a1f116a913e442bcf4 Mon Sep 17 00:00:00 2001 From: mlugg Date: Thu, 31 Jul 2025 10:56:49 +0100 Subject: [PATCH 2/2] std: stop relying on precision-losing coercions --- lib/std/math.zig | 10 +++++++--- lib/std/math/gamma.zig | 32 ++++++++++++++++---------------- lib/std/math/modf.zig | 2 +- lib/std/math/pow.zig | 4 ++-- lib/std/zon/parse.zig | 6 +++--- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/lib/std/math.zig b/lib/std/math.zig index 9f2d12a65e..c36f19ec85 100644 --- a/lib/std/math.zig +++ b/lib/std/math.zig @@ -1345,11 +1345,15 @@ pub fn lossyCast(comptime T: type, value: anytype) T { } }, .float, .comptime_float => { + // In extreme cases, we probably need a language enhancement to be able to + // specify a rounding mode here to prevent `@intFromFloat` panics. + const max: @TypeOf(value) = @floatFromInt(maxInt(T)); + const min: @TypeOf(value) = @floatFromInt(minInt(T)); if (isNan(value)) { return 0; - } else if (value >= maxInt(T)) { + } else if (value >= max) { return maxInt(T); - } else if (value <= minInt(T)) { + } else if (value <= min) { return minInt(T); } else { return @intFromFloat(value); @@ -1366,7 +1370,7 @@ test lossyCast { try testing.expect(lossyCast(i16, 70000.0) == @as(i16, 32767)); try testing.expect(lossyCast(u32, @as(i16, -255)) == @as(u32, 0)); try testing.expect(lossyCast(i9, @as(u32, 200)) == @as(i9, 200)); - try testing.expect(lossyCast(u32, @as(f32, maxInt(u32))) == maxInt(u32)); + try testing.expect(lossyCast(u32, @as(f32, @floatFromInt(maxInt(u32)))) == maxInt(u32)); try testing.expect(lossyCast(u32, nan(f32)) == 0); } diff --git a/lib/std/math/gamma.zig b/lib/std/math/gamma.zig index 5577f71461..ce9a2b07f9 100644 --- a/lib/std/math/gamma.zig +++ b/lib/std/math/gamma.zig @@ -189,19 +189,19 @@ fn series(comptime T: type, abs: T) T { 2.5066282746310002701649081771338373386264310793408, }; const denominator = [_]T{ - 0, - 39916800, - 120543840, - 150917976, - 105258076, - 45995730, - 13339535, - 2637558, - 357423, - 32670, - 1925, - 66, - 1, + 0.0, + 39916800.0, + 120543840.0, + 150917976.0, + 105258076.0, + 45995730.0, + 13339535.0, + 2637558.0, + 357423.0, + 32670.0, + 1925.0, + 66.0, + 1.0, }; var num: T = 0; var den: T = 0; @@ -244,9 +244,9 @@ const expectApproxEqRel = std.testing.expectApproxEqRel; test gamma { inline for (&.{ f32, f64 }) |T| { const eps = @sqrt(std.math.floatEps(T)); - try expectApproxEqRel(@as(T, 120), gamma(T, 6), eps); - try expectApproxEqRel(@as(T, 362880), gamma(T, 10), eps); - try expectApproxEqRel(@as(T, 6402373705728000), gamma(T, 19), eps); + try expectApproxEqRel(@as(T, 120.0), gamma(T, 6), eps); + try expectApproxEqRel(@as(T, 362880.0), gamma(T, 10), eps); + try expectApproxEqRel(@as(T, 6402373705728000.0), gamma(T, 19), eps); try expectApproxEqRel(@as(T, 332.7590766955334570), gamma(T, 0.003), eps); try expectApproxEqRel(@as(T, 1.377260301981044573), gamma(T, 0.654), eps); diff --git a/lib/std/math/modf.zig b/lib/std/math/modf.zig index 77d58bd34e..dda34454e3 100644 --- a/lib/std/math/modf.zig +++ b/lib/std/math/modf.zig @@ -74,7 +74,7 @@ fn ModfTests(comptime T: type) type { r = modf(@as(T, 43874.3)); try expectEqual(43874.0, r.ipart); // account for precision error - const expected_b: T = 43874.3 - @as(T, 43874); + const expected_b: T = 43874.3 - @as(T, 43874.0); try expectApproxEqAbs(expected_b, r.fpart, epsilon); r = modf(@as(T, 1234.340780)); diff --git a/lib/std/math/pow.zig b/lib/std/math/pow.zig index acaafe7609..42f28ce465 100644 --- a/lib/std/math/pow.zig +++ b/lib/std/math/pow.zig @@ -192,8 +192,8 @@ fn isOddInteger(x: f64) bool { } test isOddInteger { - try expect(isOddInteger(math.maxInt(i64) * 2) == false); - try expect(isOddInteger(math.maxInt(i64) * 2 + 1) == false); + try expect(isOddInteger(@floatFromInt(math.maxInt(i64) * 2)) == false); + try expect(isOddInteger(@floatFromInt(math.maxInt(i64) * 2 + 1)) == false); try expect(isOddInteger(1 << 53) == false); try expect(isOddInteger(12.0) == false); try expect(isOddInteger(15.0) == true); diff --git a/lib/std/zon/parse.zig b/lib/std/zon/parse.zig index 96a7fa6595..5f74400c29 100644 --- a/lib/std/zon/parse.zig +++ b/lib/std/zon/parse.zig @@ -2774,11 +2774,11 @@ test "std.zon parse float" { // Test big integers try std.testing.expectEqual( - @as(f32, 36893488147419103231), + @as(f32, 36893488147419103231.0), try fromSlice(f32, gpa, "36893488147419103231", null, .{}), ); try std.testing.expectEqual( - @as(f32, -36893488147419103231), + @as(f32, -36893488147419103231.0), try fromSlice(f32, gpa, "-36893488147419103231", null, .{}), ); try std.testing.expectEqual(@as(f128, 0x1ffffffffffffffff), try fromSlice( @@ -2788,7 +2788,7 @@ test "std.zon parse float" { null, .{}, )); - try std.testing.expectEqual(@as(f32, 0x1ffffffffffffffff), try fromSlice( + try std.testing.expectEqual(@as(f32, @floatFromInt(0x1ffffffffffffffff)), try fromSlice( f32, gpa, "0x1ffffffffffffffff",