Commit 6ffa285fc3

mlugg <mlugg@mlugg.co.uk>
2025-06-15 15:18:37
compiler: fix `@intFromFloat` safety check
This safety check was completely broken; it triggered unchecked illegal behavior *in order to implement the safety check*. You definitely can't do that! Instead, we must explicitly check the boundaries. This is a tiny bit fiddly, because we need to make sure we do floating-point rounding in the correct direction, and also handle the fact that the operation truncates so the boundary works differently for min vs max. Instead of implementing this safety check in Sema, there are now dedicated AIR instructions for safety-checked intfromfloat (two instructions; which one is used depends on the float mode). Currently, no backend directly implements them; instead, a `Legalize.Feature` is added which expands the safety check, and this feature is enabled for all backends we currently test, including the LLVM backend. The `u0` case is still handled in Sema, because Sema needs to check for that anyway due to the comptime-known result. The old safety check here was also completely broken and has therefore been rewritten. In that case, we just check for 'abs(input) < 1.0'. I've added a bunch of test coverage for the boundary cases of `@intFromFloat`, both for successes (in `test/behavior/cast.zig`) and failures (in `test/cases/safety/`). Resolves: #24161
1 parent 6b41beb
src/Air/Liveness/Verify.zig
@@ -107,6 +107,8 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
             .array_to_slice,
             .int_from_float,
             .int_from_float_optimized,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             .float_from_int,
             .get_union_tag,
             .clz,
src/Air/Legalize.zig
@@ -112,6 +112,8 @@ pub const Feature = enum {
     scalarize_trunc,
     scalarize_int_from_float,
     scalarize_int_from_float_optimized,
+    scalarize_int_from_float_safe,
+    scalarize_int_from_float_optimized_safe,
     scalarize_float_from_int,
     scalarize_shuffle_one,
     scalarize_shuffle_two,
@@ -126,6 +128,12 @@ pub const Feature = enum {
     /// Replace `intcast_safe` with an explicit safety check which `call`s the panic function on failure.
     /// Not compatible with `scalarize_intcast_safe`.
     expand_intcast_safe,
+    /// Replace `int_from_float_safe` with an explicit safety check which `call`s the panic function on failure.
+    /// Not compatible with `scalarize_int_from_float_safe`.
+    expand_int_from_float_safe,
+    /// Replace `int_from_float_optimized_safe` with an explicit safety check which `call`s the panic function on failure.
+    /// Not compatible with `scalarize_int_from_float_optimized_safe`.
+    expand_int_from_float_optimized_safe,
     /// Replace `add_safe` with an explicit safety check which `call`s the panic function on failure.
     /// Not compatible with `scalarize_add_safe`.
     expand_add_safe,
@@ -225,6 +233,8 @@ pub const Feature = enum {
             .trunc => .scalarize_trunc,
             .int_from_float => .scalarize_int_from_float,
             .int_from_float_optimized => .scalarize_int_from_float_optimized,
+            .int_from_float_safe => .scalarize_int_from_float_safe,
+            .int_from_float_optimized_safe => .scalarize_int_from_float_optimized_safe,
             .float_from_int => .scalarize_float_from_int,
             .shuffle_one => .scalarize_shuffle_one,
             .shuffle_two => .scalarize_shuffle_two,
@@ -439,6 +449,20 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
                 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
                 if (ty_op.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .ty_op);
             },
+            .int_from_float_safe => if (l.features.has(.expand_int_from_float_safe)) {
+                assert(!l.features.has(.scalarize_int_from_float_safe));
+                continue :inst l.replaceInst(inst, .block, try l.safeIntFromFloatBlockPayload(inst, false));
+            } else if (l.features.has(.scalarize_int_from_float_safe)) {
+                const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
+                if (ty_op.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .ty_op);
+            },
+            .int_from_float_optimized_safe => if (l.features.has(.expand_int_from_float_optimized_safe)) {
+                assert(!l.features.has(.scalarize_int_from_float_optimized_safe));
+                continue :inst l.replaceInst(inst, .block, try l.safeIntFromFloatBlockPayload(inst, true));
+            } else if (l.features.has(.scalarize_int_from_float_optimized_safe)) {
+                const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
+                if (ty_op.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .ty_op);
+            },
             .block, .loop => {
                 const ty_pl = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_pl;
                 const extra = l.extraData(Air.Block, ty_pl.payload);
@@ -2001,6 +2025,115 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In
         .payload = try l.addBlockBody(main_block.body()),
     } };
 }
+fn safeIntFromFloatBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, optimized: bool) Error!Air.Inst.Data {
+    const pt = l.pt;
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;
+
+    const operand_ref = ty_op.operand;
+    const operand_ty = l.typeOf(operand_ref);
+    const dest_ty = ty_op.ty.toType();
+
+    const is_vector = operand_ty.zigTypeTag(zcu) == .vector;
+    const dest_scalar_ty = dest_ty.scalarType(zcu);
+    const int_info = dest_scalar_ty.intInfo(zcu);
+
+    // We emit 9 instructions in the worst case.
+    var inst_buf: [9]Air.Inst.Index = undefined;
+    try l.air_instructions.ensureUnusedCapacity(zcu.gpa, inst_buf.len);
+    var main_block: Block = .init(&inst_buf);
+
+    // This check is a bit annoying because of floating-point rounding and the fact that this
+    // builtin truncates. We'll use a bigint for our calculations, because we need to construct
+    // integers exceeding the bounds of the result integer type, and we need to convert it to a
+    // float with a specific rounding mode to avoid errors.
+    // Our bigint may exceed the twos complement limit by one, so add an extra limb.
+    const limbs = try gpa.alloc(
+        std.math.big.Limb,
+        std.math.big.int.calcTwosCompLimbCount(int_info.bits) + 1,
+    );
+    defer gpa.free(limbs);
+    var big: std.math.big.int.Mutable = .init(limbs, 0);
+
+    // Check if the operand is lower than `min_int` when truncated to an integer.
+    big.setTwosCompIntLimit(.min, int_info.signedness, int_info.bits);
+    const below_min_inst: Air.Inst.Index = if (!big.positive or big.eqlZero()) bad: {
+        // `min_int <= 0`, so check for `x <= min_int - 1`.
+        big.addScalar(big.toConst(), -1);
+        // For `<=`, we must round the RHS down, so that this value is the first `x` which returns `true`.
+        const limit_val = try floatFromBigIntVal(pt, is_vector, operand_ty, big.toConst(), .floor);
+        break :bad try main_block.addCmp(l, .lte, operand_ref, Air.internedToRef(limit_val.toIntern()), .{
+            .vector = is_vector,
+            .optimized = optimized,
+        });
+    } else {
+        // `min_int > 0`, which is currently impossible. It would become possible under #3806, in
+        // which case we must detect `x < min_int`.
+        unreachable;
+    };
+
+    // Check if the operand is greater than `max_int` when truncated to an integer.
+    big.setTwosCompIntLimit(.max, int_info.signedness, int_info.bits);
+    const above_max_inst: Air.Inst.Index = if (big.positive or big.eqlZero()) bad: {
+        // `max_int >= 0`, so check for `x >= max_int + 1`.
+        big.addScalar(big.toConst(), 1);
+        // For `>=`, we must round the RHS up, so that this value is the first `x` which returns `true`.
+        const limit_val = try floatFromBigIntVal(pt, is_vector, operand_ty, big.toConst(), .ceil);
+        break :bad try main_block.addCmp(l, .gte, operand_ref, Air.internedToRef(limit_val.toIntern()), .{
+            .vector = is_vector,
+            .optimized = optimized,
+        });
+    } else {
+        // `max_int < 0`, which is currently impossible. It would become possible under #3806, in
+        // which case we must detect `x > max_int`.
+        unreachable;
+    };
+
+    // Combine the conditions.
+    const out_of_bounds_inst: Air.Inst.Index = main_block.add(l, .{
+        .tag = .bool_or,
+        .data = .{ .bin_op = .{
+            .lhs = below_min_inst.toRef(),
+            .rhs = above_max_inst.toRef(),
+        } },
+    });
+    const scalar_out_of_bounds_inst: Air.Inst.Index = if (is_vector) main_block.add(l, .{
+        .tag = .reduce,
+        .data = .{ .reduce = .{
+            .operand = out_of_bounds_inst.toRef(),
+            .operation = .Or,
+        } },
+    }) else out_of_bounds_inst;
+
+    // Now emit the actual condbr. "true" will be safety panic. "false" will be "ok", meaning we do
+    // the `int_from_float` and `br` the result to `orig_inst`.
+    var condbr: CondBr = .init(l, scalar_out_of_bounds_inst.toRef(), &main_block, .{ .true = .cold });
+    condbr.then_block = .init(main_block.stealRemainingCapacity());
+    try condbr.then_block.addPanic(l, .integer_part_out_of_bounds);
+    condbr.else_block = .init(condbr.then_block.stealRemainingCapacity());
+    const cast_inst = condbr.else_block.add(l, .{
+        .tag = if (optimized) .int_from_float_optimized else .int_from_float,
+        .data = .{ .ty_op = .{
+            .ty = Air.internedToRef(dest_ty.toIntern()),
+            .operand = operand_ref,
+        } },
+    });
+    _ = condbr.else_block.add(l, .{
+        .tag = .br,
+        .data = .{ .br = .{
+            .block_inst = orig_inst,
+            .operand = cast_inst.toRef(),
+        } },
+    });
+    _ = condbr.else_block.stealRemainingCapacity(); // we might not have used it all
+    try condbr.finish(l);
+
+    return .{ .ty_pl = .{
+        .ty = Air.internedToRef(dest_ty.toIntern()),
+        .payload = try l.addBlockBody(main_block.body()),
+    } };
+}
 fn safeArithmeticBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, overflow_op_tag: Air.Inst.Tag) Error!Air.Inst.Data {
     const pt = l.pt;
     const zcu = pt.zcu;
@@ -2378,6 +2511,42 @@ fn packedAggregateInitBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Erro
     } };
 }
 
+/// Given a `std.math.big.int.Const`, converts it to a `Value` which is a float of type `float_ty`
+/// representing the same numeric value. If the integer cannot be exactly represented, `round`
+/// decides whether the value should be rounded up or down. If `is_vector`, then `float_ty` is
+/// instead a vector of floats, and the result value is a vector containing the converted scalar
+/// repeated N times.
+fn floatFromBigIntVal(
+    pt: Zcu.PerThread,
+    is_vector: bool,
+    float_ty: Type,
+    x: std.math.big.int.Const,
+    round: std.math.big.int.Round,
+) Error!Value {
+    const zcu = pt.zcu;
+    const scalar_ty = switch (is_vector) {
+        true => float_ty.childType(zcu),
+        false => float_ty,
+    };
+    assert(scalar_ty.zigTypeTag(zcu) == .float);
+    const scalar_val: Value = switch (scalar_ty.floatBits(zcu.getTarget())) {
+        16 => try pt.floatValue(scalar_ty, x.toFloat(f16, round)[0]),
+        32 => try pt.floatValue(scalar_ty, x.toFloat(f32, round)[0]),
+        64 => try pt.floatValue(scalar_ty, x.toFloat(f64, round)[0]),
+        80 => try pt.floatValue(scalar_ty, x.toFloat(f80, round)[0]),
+        128 => try pt.floatValue(scalar_ty, x.toFloat(f128, round)[0]),
+        else => unreachable,
+    };
+    if (is_vector) {
+        return .fromInterned(try pt.intern(.{ .aggregate = .{
+            .ty = float_ty.toIntern(),
+            .storage = .{ .repeated_elem = scalar_val.toIntern() },
+        } }));
+    } else {
+        return scalar_val;
+    }
+}
+
 const Block = struct {
     instructions: []Air.Inst.Index,
     len: usize,
@@ -2735,4 +2904,5 @@ const InternPool = @import("../InternPool.zig");
 const Legalize = @This();
 const std = @import("std");
 const Type = @import("../Type.zig");
+const Value = @import("../Value.zig");
 const Zcu = @import("../Zcu.zig");
src/Air/Liveness.zig
@@ -374,6 +374,8 @@ pub fn categorizeOperand(
         .array_to_slice,
         .int_from_float,
         .int_from_float_optimized,
+        .int_from_float_safe,
+        .int_from_float_optimized_safe,
         .float_from_int,
         .get_union_tag,
         .clz,
@@ -1015,6 +1017,8 @@ fn analyzeInst(
         .array_to_slice,
         .int_from_float,
         .int_from_float_optimized,
+        .int_from_float_safe,
+        .int_from_float_optimized_safe,
         .float_from_int,
         .get_union_tag,
         .clz,
src/Air/print.zig
@@ -250,6 +250,8 @@ const Writer = struct {
             .splat,
             .int_from_float,
             .int_from_float_optimized,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             .get_union_tag,
             .clz,
             .ctz,
src/Air/types_resolved.zig
@@ -130,6 +130,8 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
             .array_to_slice,
             .int_from_float,
             .int_from_float_optimized,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             .float_from_int,
             .splat,
             .error_set_has_value,
src/arch/aarch64/CodeGen.zig
@@ -861,6 +861,8 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .sub_safe,
             .mul_safe,
             .intcast_safe,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             => return self.fail("TODO implement safety_checked_instructions", .{}),
 
             .is_named_enum_value => return self.fail("TODO implement is_named_enum_value", .{}),
src/arch/arm/CodeGen.zig
@@ -850,6 +850,8 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .sub_safe,
             .mul_safe,
             .intcast_safe,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             => return self.fail("TODO implement safety_checked_instructions", .{}),
 
             .is_named_enum_value => return self.fail("TODO implement is_named_enum_value", .{}),
src/arch/riscv64/CodeGen.zig
@@ -54,6 +54,8 @@ const InnerError = CodeGenError || error{OutOfRegisters};
 pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
     return comptime &.initMany(&.{
         .expand_intcast_safe,
+        .expand_int_from_float_safe,
+        .expand_int_from_float_optimized_safe,
         .expand_add_safe,
         .expand_sub_safe,
         .expand_mul_safe,
@@ -1474,6 +1476,8 @@ fn genBody(func: *Func, body: []const Air.Inst.Index) InnerError!void {
             .sub_safe,
             .mul_safe,
             .intcast_safe,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             => return func.fail("TODO implement safety_checked_instructions", .{}),
 
             .cmp_lt,
src/arch/sparc64/CodeGen.zig
@@ -696,6 +696,8 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .sub_safe,
             .mul_safe,
             .intcast_safe,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             => @panic("TODO implement safety_checked_instructions"),
 
             .is_named_enum_value => @panic("TODO implement is_named_enum_value"),
src/arch/wasm/CodeGen.zig
@@ -31,6 +31,8 @@ const compilerRtIntAbbrev = target_util.compilerRtIntAbbrev;
 pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
     return comptime &.initMany(&.{
         .expand_intcast_safe,
+        .expand_int_from_float_safe,
+        .expand_int_from_float_optimized_safe,
         .expand_add_safe,
         .expand_sub_safe,
         .expand_mul_safe,
@@ -2020,6 +2022,8 @@ fn genInst(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void {
         .sub_safe,
         .mul_safe,
         .intcast_safe,
+        .int_from_float_safe,
+        .int_from_float_optimized_safe,
         => return cg.fail("TODO implement safety_checked_instructions", .{}),
 
         .work_item_id,
src/arch/x86_64/CodeGen.zig
@@ -102,6 +102,8 @@ pub fn legalizeFeatures(target: *const std.Target) *const Air.Legalize.Features
             .reduce_one_elem_to_bitcast = true,
 
             .expand_intcast_safe = true,
+            .expand_int_from_float_safe = true,
+            .expand_int_from_float_optimized_safe = true,
             .expand_add_safe = true,
             .expand_sub_safe = true,
             .expand_mul_safe = true,
@@ -107763,6 +107765,8 @@ fn genBody(cg: *CodeGen, body: []const Air.Inst.Index) InnerError!void {
                 };
                 try res[0].finish(inst, &.{ty_op.operand}, &ops, cg);
             },
+            .int_from_float_safe => unreachable,
+            .int_from_float_optimized_safe => unreachable,
             .float_from_int => |air_tag| if (use_old) try cg.airFloatFromInt(inst) else {
                 const ty_op = air_datas[@intFromEnum(inst)].ty_op;
                 var ops = try cg.tempsFromOperands(inst, .{ty_op.operand});
src/codegen/c.zig
@@ -27,6 +27,8 @@ pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features {
         inline false, true => |supports_legalize| &.init(.{
             // we don't currently ask zig1 to use safe optimization modes
             .expand_intcast_safe = supports_legalize,
+            .expand_int_from_float_safe = supports_legalize,
+            .expand_int_from_float_optimized_safe = supports_legalize,
             .expand_add_safe = supports_legalize,
             .expand_sub_safe = supports_legalize,
             .expand_mul_safe = supports_legalize,
@@ -3578,6 +3580,8 @@ fn genBodyInner(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail,
             .sub_safe,
             .mul_safe,
             .intcast_safe,
+            .int_from_float_safe,
+            .int_from_float_optimized_safe,
             => return f.fail("TODO implement safety_checked_instructions", .{}),
 
             .is_named_enum_value => return f.fail("TODO: C backend: implement is_named_enum_value", .{}),
src/codegen/llvm.zig
@@ -37,7 +37,10 @@ const compilerRtIntAbbrev = target_util.compilerRtIntAbbrev;
 const Error = error{ OutOfMemory, CodegenFail };
 
 pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features {
-    return null;
+    return comptime &.initMany(&.{
+        .expand_int_from_float_safe,
+        .expand_int_from_float_optimized_safe,
+    });
 }
 
 fn subArchName(target: std.Target, comptime family: std.Target.Cpu.Arch.Family, mappings: anytype) ?[]const u8 {
@@ -4987,6 +4990,8 @@ pub const FuncGen = struct {
 
                 .int_from_float           => try self.airIntFromFloat(inst, .normal),
                 .int_from_float_optimized => try self.airIntFromFloat(inst, .fast),
+                .int_from_float_safe           => unreachable, // handled by `legalizeFeatures`
+                .int_from_float_optimized_safe => unreachable, // handled by `legalizeFeatures`
 
                 .array_to_slice => try self.airArrayToSlice(inst),
                 .float_from_int => try self.airFloatFromInt(inst),
src/codegen/spirv.zig
@@ -31,6 +31,8 @@ const InstMap = std.AutoHashMapUnmanaged(Air.Inst.Index, IdRef);
 pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
     return comptime &.initMany(&.{
         .expand_intcast_safe,
+        .expand_int_from_float_safe,
+        .expand_int_from_float_optimized_safe,
         .expand_add_safe,
         .expand_sub_safe,
         .expand_mul_safe,
src/Air.zig
@@ -683,6 +683,10 @@ pub const Inst = struct {
         int_from_float,
         /// Same as `int_from_float` with optimized float mode.
         int_from_float_optimized,
+        /// Same as `int_from_float`, but with a safety check that the operand is in bounds.
+        int_from_float_safe,
+        /// Same as `int_from_float_optimized`, but with a safety check that the operand is in bounds.
+        int_from_float_optimized_safe,
         /// Given an integer operand, return the float with the closest mathematical meaning.
         /// Uses the `ty_op` field.
         float_from_int,
@@ -1612,6 +1616,8 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
         .array_to_slice,
         .int_from_float,
         .int_from_float_optimized,
+        .int_from_float_safe,
+        .int_from_float_optimized_safe,
         .float_from_int,
         .splat,
         .get_union_tag,
@@ -1842,6 +1848,8 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
         .sub_safe,
         .mul_safe,
         .intcast_safe,
+        .int_from_float_safe,
+        .int_from_float_optimized_safe,
         => true,
 
         .add,
src/Sema.zig
@@ -22178,44 +22178,34 @@ fn zirIntFromFloat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro
 
     try sema.requireRuntimeBlock(block, src, operand_src);
     if (dest_scalar_ty.intInfo(zcu).bits == 0) {
-        if (!is_vector) {
-            if (block.wantSafety()) {
-                const ok = try block.addBinOp(if (block.float_mode == .optimized) .cmp_eq_optimized else .cmp_eq, operand, Air.internedToRef((try pt.floatValue(operand_ty, 0.0)).toIntern()));
-                try sema.addSafetyCheck(block, src, ok, .integer_part_out_of_bounds);
-            }
-            return Air.internedToRef((try pt.intValue(dest_ty, 0)).toIntern());
-        }
         if (block.wantSafety()) {
-            const len = dest_ty.vectorLen(zcu);
-            for (0..len) |i| {
-                const idx_ref = try pt.intRef(.usize, i);
-                const elem_ref = try block.addBinOp(.array_elem_val, operand, idx_ref);
-                const ok = try block.addBinOp(if (block.float_mode == .optimized) .cmp_eq_optimized else .cmp_eq, elem_ref, Air.internedToRef((try pt.floatValue(operand_scalar_ty, 0.0)).toIntern()));
-                try sema.addSafetyCheck(block, src, ok, .integer_part_out_of_bounds);
-            }
-        }
+            // Emit an explicit safety check. We can do this one like `abs(x) < 1`.
+            const abs_ref = try block.addTyOp(.abs, operand_ty, operand);
+            const max_abs_ref = if (is_vector) try block.addReduce(abs_ref, .Max) else abs_ref;
+            const one_ref = Air.internedToRef((try pt.floatValue(operand_scalar_ty, 1.0)).toIntern());
+            const ok_ref = try block.addBinOp(.cmp_lt, max_abs_ref, one_ref);
+            try sema.addSafetyCheck(block, src, ok_ref, .integer_part_out_of_bounds);
+        }
+        const scalar_val = try pt.intValue(dest_scalar_ty, 0);
+        if (!is_vector) return Air.internedToRef(scalar_val.toIntern());
         return Air.internedToRef(try pt.intern(.{ .aggregate = .{
             .ty = dest_ty.toIntern(),
-            .storage = .{ .repeated_elem = (try pt.intValue(dest_scalar_ty, 0)).toIntern() },
+            .storage = .{ .repeated_elem = scalar_val.toIntern() },
         } }));
     }
-    const result = try block.addTyOp(if (block.float_mode == .optimized) .int_from_float_optimized else .int_from_float, dest_ty, operand);
     if (block.wantSafety()) {
-        const back = try block.addTyOp(.float_from_int, operand_ty, result);
-        const diff = try block.addBinOp(if (block.float_mode == .optimized) .sub_optimized else .sub, operand, back);
-        const ok = if (is_vector) ok: {
-            const ok_pos = try block.addCmpVector(diff, Air.internedToRef((try sema.splat(operand_ty, try pt.floatValue(operand_scalar_ty, 1.0))).toIntern()), .lt);
-            const ok_neg = try block.addCmpVector(diff, Air.internedToRef((try sema.splat(operand_ty, try pt.floatValue(operand_scalar_ty, -1.0))).toIntern()), .gt);
-            const ok = try block.addBinOp(.bit_and, ok_pos, ok_neg);
-            break :ok try block.addReduce(ok, .And);
-        } else ok: {
-            const ok_pos = try block.addBinOp(if (block.float_mode == .optimized) .cmp_lt_optimized else .cmp_lt, diff, Air.internedToRef((try pt.floatValue(operand_ty, 1.0)).toIntern()));
-            const ok_neg = try block.addBinOp(if (block.float_mode == .optimized) .cmp_gt_optimized else .cmp_gt, diff, Air.internedToRef((try pt.floatValue(operand_ty, -1.0)).toIntern()));
-            break :ok try block.addBinOp(.bool_and, ok_pos, ok_neg);
-        };
-        try sema.addSafetyCheck(block, src, ok, .integer_part_out_of_bounds);
+        if (zcu.backendSupportsFeature(.panic_fn)) {
+            _ = try sema.preparePanicId(src, .integer_part_out_of_bounds);
+        }
+        return block.addTyOp(switch (block.float_mode) {
+            .optimized => .int_from_float_optimized_safe,
+            .strict => .int_from_float_safe,
+        }, dest_ty, operand);
     }
-    return result;
+    return block.addTyOp(switch (block.float_mode) {
+        .optimized => .int_from_float_optimized,
+        .strict => .int_from_float,
+    }, dest_ty, operand);
 }
 
 fn zirFloatFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
test/behavior/cast.zig
@@ -102,6 +102,7 @@ test "comptime_int @floatFromInt" {
 test "@floatFromInt" {
     if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
     if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
     if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
 
     const S = struct {
@@ -2737,3 +2738,66 @@ test "peer type resolution: slice of sentinel-terminated array" {
     try expect(result[0][0] == 10);
     try expect(result[0][1] == 20);
 }
+
+test "@intFromFloat boundary cases" {
+    if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
+
+    const S = struct {
+        fn case(comptime I: type, x: f32, bump: enum { up, down }, expected: I) !void {
+            const input: f32 = switch (bump) {
+                .up => std.math.nextAfter(f32, x, std.math.inf(f32)),
+                .down => std.math.nextAfter(f32, x, -std.math.inf(f32)),
+            };
+            const output: I = @intFromFloat(input);
+            try expect(output == expected);
+        }
+        fn doTheTest() !void {
+            try case(u8, 256.0, .down, 255);
+            try case(u8, -1.0, .up, 0);
+            try case(i8, 128.0, .down, 127);
+            try case(i8, -129.0, .up, -128);
+
+            try case(u0, 1.0, .down, 0);
+            try case(u0, -1.0, .up, 0);
+            try case(i0, 1.0, .down, 0);
+            try case(i0, -1.0, .up, 0);
+
+            try case(u10, 1024.0, .down, 1023);
+            try case(u10, -1.0, .up, 0);
+            try case(i10, 512.0, .down, 511);
+            try case(i10, -513.0, .up, -512);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
+
+test "@intFromFloat vector boundary cases" {
+    if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest;
+    if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
+
+    const S = struct {
+        fn case(comptime I: type, unshifted_inputs: [2]f32, expected: [2]I) !void {
+            const inputs: @Vector(2, f32) = .{
+                std.math.nextAfter(f32, unshifted_inputs[0], std.math.inf(f32)),
+                std.math.nextAfter(f32, unshifted_inputs[1], -std.math.inf(f32)),
+            };
+            const outputs: @Vector(2, I) = @intFromFloat(inputs);
+            try expect(outputs[0] == expected[0]);
+            try expect(outputs[1] == expected[1]);
+        }
+        fn doTheTest() !void {
+            try case(u8, .{ -1.0, 256.0 }, .{ 0, 255 });
+            try case(i8, .{ -129.0, 128.0 }, .{ -128, 127 });
+
+            try case(u0, .{ -1.0, 1.0 }, .{ 0, 0 });
+            try case(i0, .{ -1.0, 1.0 }, .{ 0, 0 });
+
+            try case(u10, .{ -1.0, 1024.0 }, .{ 0, 1023 });
+            try case(i10, .{ -513.0, 512.0 }, .{ -512, 511 });
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
test/cases/safety/@intFromFloat cannot fit - boundary case - i0 max.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = 1.0;
+pub fn main() !void {
+    _ = @as(i0, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - i0 min.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = -1.0;
+pub fn main() !void {
+    _ = @as(i0, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - signed max.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = 128;
+pub fn main() !void {
+    _ = @as(i8, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - signed min.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = -129;
+pub fn main() !void {
+    _ = @as(i8, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - u0 max.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = 1.0;
+pub fn main() !void {
+    _ = @as(u0, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - u0 min.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = -1.0;
+pub fn main() !void {
+    _ = @as(u0, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned max.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = 256;
+pub fn main() !void {
+    _ = @as(u8, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned min.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: f32 = -1;
+pub fn main() !void {
+    _ = @as(u8, @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - vector max.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: @Vector(2, f32) = .{ 100, 512 };
+pub fn main() !void {
+    _ = @as(@Vector(2, i10), @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - vector min.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    _ = stack_trace;
+    if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
+        std.process.exit(0);
+    }
+    std.process.exit(1);
+}
+var x: @Vector(2, f32) = .{ 100, -513 };
+pub fn main() !void {
+    _ = @as(@Vector(2, i10), @intFromFloat(x));
+    return error.TestFailed;
+}
+// run
+// backend=stage2,llvm
+// target=native