From 47bb2103171c38afe926c7e678322c9e54e15021 Mon Sep 17 00:00:00 2001 From: Jay Petacat Date: Thu, 4 Dec 2025 11:40:03 -0700 Subject: [PATCH] Sema: Allow small integer types to coerce to floats If the float can store all possible values of the integer without rounding, coercion is allowed. The integer's precision must be less than or equal to the float's significand precision. Closes #18614 --- doc/langref.html.in | 10 ++++ .../test_failed_int_to_float_coercion.zig | 8 +++ doc/langref/test_int_to_float_coercion.zig | 12 +++++ src/Sema.zig | 18 ++++++- test/behavior/cast.zig | 54 +++++++++++++++++++ .../compile_errors/coerce_int_to_float.zig | 52 ++++++++++++++++++ 6 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 doc/langref/test_failed_int_to_float_coercion.zig create mode 100644 doc/langref/test_int_to_float_coercion.zig create mode 100644 test/cases/compile_errors/coerce_int_to_float.zig diff --git a/doc/langref.html.in b/doc/langref.html.in index c0272124ca..126f9ad854 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -3449,6 +3449,16 @@ void do_a_thing(struct Foo *foo) {

{#code|test_integer_widening.zig#} + {#header_close#} + {#header_open|Type Coercion: Int to Float#} +

+ {#link|Integers#} coerce to {#link|Floats#} if every possible integer value can be stored in the float + without rounding (i.e. the integer's precision does not exceed the float's significand precision). + Larger integer types that cannot be safely coerced must be explicitly casted with {#link|@floatFromInt#}. +

+ {#code|test_int_to_float_coercion.zig#} + {#code|test_failed_int_to_float_coercion.zig#} + {#header_close#} {#header_open|Type Coercion: Float to Int#}

diff --git a/doc/langref/test_failed_int_to_float_coercion.zig b/doc/langref/test_failed_int_to_float_coercion.zig new file mode 100644 index 0000000000..0d24e21927 --- /dev/null +++ b/doc/langref/test_failed_int_to_float_coercion.zig @@ -0,0 +1,8 @@ +test "integer type is too large for implicit cast to float" { + var int: u25 = 123; + _ = ∫ + const float: f32 = int; + _ = float; +} + +// test_error= diff --git a/doc/langref/test_int_to_float_coercion.zig b/doc/langref/test_int_to_float_coercion.zig new file mode 100644 index 0000000000..d2a8d903e5 --- /dev/null +++ b/doc/langref/test_int_to_float_coercion.zig @@ -0,0 +1,12 @@ +const std = @import("std"); +const expectEqual = std.testing.expectEqual; + +test "implicit integer to float" { + var int: u8 = 123; + _ = ∫ + const float: f32 = int; + const int_from_float: u8 = @intFromFloat(float); + try expectEqual(int, int_from_float); +} + +// test diff --git a/src/Sema.zig b/src/Sema.zig index d31447544c..085aa343fb 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -28479,7 +28479,7 @@ pub fn coerce( }; } -const CoersionError = CompileError || error{ +const CoercionError = CompileError || error{ /// When coerce is called recursively, this error should be returned instead of using `fail` /// to ensure correct types in compile errors. NotCoercible, @@ -28518,7 +28518,7 @@ fn coerceExtra( inst: Air.Inst.Ref, inst_src: LazySrcLoc, opts: CoerceOpts, -) CoersionError!Air.Inst.Ref { +) CoercionError!Air.Inst.Ref { if (dest_ty.isGenericPoison()) return inst; const pt = sema.pt; const zcu = pt.zcu; @@ -28945,6 +28945,20 @@ fn coerceExtra( if (!opts.report_err) return error.NotCoercible; return sema.failWithNeededComptime(block, inst_src, .{ .simple = .casted_to_comptime_float }); } + const int_info = inst_ty.intInfo(zcu); + const int_precision = int_info.bits - @intFromBool(int_info.signedness == .signed); + const float_precision: u8 = switch (dest_ty.toIntern()) { + .f16_type => 11, + .f32_type => 24, + .f64_type => 53, + .f80_type => 64, + .f128_type => 113, + else => unreachable, + }; + if (int_precision <= float_precision) { + try sema.requireRuntimeBlock(block, inst_src, null); + return block.addTyOp(.float_from_int, dest_ty, inst); + } break :int; }; const result_val = try val.floatFromIntAdvanced(sema.arena, inst_ty, dest_ty, pt, .sema); diff --git a/test/behavior/cast.zig b/test/behavior/cast.zig index 4796c0c7ae..0486659b54 100644 --- a/test/behavior/cast.zig +++ b/test/behavior/cast.zig @@ -157,6 +157,60 @@ test "@floatFromInt(f80)" { try comptime S.doTheTest(i256); } +test "type coercion from int to float" { + const check = struct { + // Check that an integer value can be coerced to a float type and + // then converted back to the original value without rounding issues. + fn value(Float: type, int: anytype) !void { + const float: Float = int; + const Int = @TypeOf(int); + try std.testing.expectEqual(int, @as(Int, @intFromFloat(float))); + if (Float != f80) { // https://codeberg.org/ziglang/zig/issues/30035 + try std.testing.expectEqual(int, @as(Int, @intFromFloat(@ceil(float)))); + try std.testing.expectEqual(int, @as(Int, @intFromFloat(@floor(float)))); + } + } + + // Exhaustively check that all possible values of the integer type can + // safely be coerced to the float type. + fn allValues(Float: type, Int: type) !void { + var int: Int = std.math.minInt(Int); + while (int < std.math.maxInt(Int)) : (int += 1) + try value(Float, int); + } + + // Check that the min and max values of the integer type can safely be + // coerced to the float type. + fn edgeValues(Float: type, Int: type) !void { + var int: Int = std.math.minInt(Int); + try value(Float, int); + int = std.math.maxInt(Int); + try value(Float, int); + } + }; + + try check.allValues(f16, u11); + try check.allValues(f16, i12); + + try check.edgeValues(f32, u24); + try check.edgeValues(f32, i25); + + try check.edgeValues(f64, u53); + try check.edgeValues(f64, i54); + + try check.edgeValues(f80, u64); + try check.edgeValues(f80, i65); + + try check.edgeValues(f128, u113); + try check.edgeValues(f128, i114); + + if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; + + // Basic sanity check that the coercions work for vectors too. + const int_vec: @Vector(2, u24) = @splat(123); + try check.value(@Vector(2, f32), int_vec); +} + test "@intFromFloat" { if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO diff --git a/test/cases/compile_errors/coerce_int_to_float.zig b/test/cases/compile_errors/coerce_int_to_float.zig new file mode 100644 index 0000000000..bd167b36ad --- /dev/null +++ b/test/cases/compile_errors/coerce_int_to_float.zig @@ -0,0 +1,52 @@ +// Test that integer types above a certain size will not coerce to a float. + +fn testCoerce(Float: type, Int: type) void { + var i: Int = 0; + _ = &i; + _ = @as(Float, i); +} + +export fn entry() void { + testCoerce(f16, u11); // Okay + testCoerce(f16, u12); // Too big + + testCoerce(f16, i12); + testCoerce(f16, i13); + + testCoerce(f32, u24); + testCoerce(f32, u25); + + testCoerce(f32, i25); + testCoerce(f32, i26); + + testCoerce(f64, u53); + testCoerce(f64, u54); + + testCoerce(f64, i54); + testCoerce(f64, i55); + + testCoerce(f80, u64); + testCoerce(f80, u65); + + testCoerce(f80, i65); + testCoerce(f80, i66); + + testCoerce(f128, u113); + testCoerce(f128, u114); + + testCoerce(f128, i114); + testCoerce(f128, i115); +} + +// error +// +// :6:20: error: expected type 'f16', found 'u12' +// :6:20: error: expected type 'f16', found 'i13' +// :6:20: error: expected type 'f32', found 'u25' +// :6:20: error: expected type 'f32', found 'i26' +// :6:20: error: expected type 'f64', found 'u54' +// :6:20: error: expected type 'f64', found 'i55' +// :6:20: error: expected type 'f80', found 'u65' +// :6:20: error: expected type 'f80', found 'i66' +// :6:20: error: expected type 'f128', found 'u114' +// :6:20: error: expected type 'f128', found 'i115'