From 484cc15366eb2654be07e449c69ff9b4d04d5973 Mon Sep 17 00:00:00 2001 From: Jay Petacat Date: Thu, 8 Jan 2026 23:48:25 -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 | 53 +++++++++++++++++++ .../compile_errors/coerce_int_to_float.zig | 52 ++++++++++++++++++ 6 files changed, 151 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 c3aa3c9b1b..f2508e175b 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 51021f8db1..63e923ad19 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -28715,7 +28715,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, @@ -28754,7 +28754,7 @@ fn coerceExtra( inst: Air.Inst.Ref, inst_src: LazySrcLoc, opts: CoerceOpts, -) CoersionError!Air.Inst.Ref { +) CoercionError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; const comp = zcu.comp; @@ -29186,6 +29186,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..6515df5e2f 100644 --- a/test/behavior/cast.zig +++ b/test/behavior/cast.zig @@ -157,6 +157,59 @@ 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))); + 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; + if (builtin.zig_backend == .stage2_wasm) 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'