From 6ffa285fc35651cb7f4b738a0f6e84718b821fd8 Mon Sep 17 00:00:00 2001 From: mlugg Date: Sun, 15 Jun 2025 14:18:37 +0100 Subject: [PATCH] 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 --- src/Air.zig | 8 + src/Air/Legalize.zig | 170 ++++++++++++++++++ src/Air/Liveness.zig | 4 + src/Air/Liveness/Verify.zig | 2 + src/Air/print.zig | 2 + src/Air/types_resolved.zig | 2 + src/Sema.zig | 50 +++--- src/arch/aarch64/CodeGen.zig | 2 + src/arch/arm/CodeGen.zig | 2 + src/arch/riscv64/CodeGen.zig | 4 + src/arch/sparc64/CodeGen.zig | 2 + src/arch/wasm/CodeGen.zig | 4 + src/arch/x86_64/CodeGen.zig | 4 + src/codegen/c.zig | 4 + src/codegen/llvm.zig | 7 +- src/codegen/spirv.zig | 2 + test/behavior/cast.zig | 64 +++++++ ...at cannot fit - boundary case - i0 max.zig | 16 ++ ...at cannot fit - boundary case - i0 min.zig | 16 ++ ...annot fit - boundary case - signed max.zig | 16 ++ ...annot fit - boundary case - signed min.zig | 16 ++ ...at cannot fit - boundary case - u0 max.zig | 16 ++ ...at cannot fit - boundary case - u0 min.zig | 16 ++ ...not fit - boundary case - unsigned max.zig | 16 ++ ...not fit - boundary case - unsigned min.zig | 16 ++ ...annot fit - boundary case - vector max.zig | 16 ++ ...annot fit - boundary case - vector min.zig | 16 ++ 27 files changed, 462 insertions(+), 31 deletions(-) create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - i0 max.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - i0 min.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - signed max.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - signed min.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - u0 max.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - u0 min.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned max.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned min.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - vector max.zig create mode 100644 test/cases/safety/@intFromFloat cannot fit - boundary case - vector min.zig diff --git a/src/Air.zig b/src/Air.zig index 74f4c57424..561ab86405 100644 --- a/src/Air.zig +++ b/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, diff --git a/src/Air/Legalize.zig b/src/Air/Legalize.zig index c0b0d9b76c..dafb3e5ed7 100644 --- a/src/Air/Legalize.zig +++ b/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"); diff --git a/src/Air/Liveness.zig b/src/Air/Liveness.zig index 94efaf114c..94ed60fbf2 100644 --- a/src/Air/Liveness.zig +++ b/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, diff --git a/src/Air/Liveness/Verify.zig b/src/Air/Liveness/Verify.zig index 2fafc44ab7..85345ceb66 100644 --- a/src/Air/Liveness/Verify.zig +++ b/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, diff --git a/src/Air/print.zig b/src/Air/print.zig index 7f5f396cae..53efa72356 100644 --- a/src/Air/print.zig +++ b/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, diff --git a/src/Air/types_resolved.zig b/src/Air/types_resolved.zig index 1def679794..2bf1ef108f 100644 --- a/src/Air/types_resolved.zig +++ b/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, diff --git a/src/Sema.zig b/src/Sema.zig index 953cdbf2ac..8308a17e95 100644 --- a/src/Sema.zig +++ b/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 { diff --git a/src/arch/aarch64/CodeGen.zig b/src/arch/aarch64/CodeGen.zig index 4aaf6bf85c..a8a60e7ce7 100644 --- a/src/arch/aarch64/CodeGen.zig +++ b/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", .{}), diff --git a/src/arch/arm/CodeGen.zig b/src/arch/arm/CodeGen.zig index 09304bf1de..98c1420863 100644 --- a/src/arch/arm/CodeGen.zig +++ b/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", .{}), diff --git a/src/arch/riscv64/CodeGen.zig b/src/arch/riscv64/CodeGen.zig index c82061a5dd..1743f37e84 100644 --- a/src/arch/riscv64/CodeGen.zig +++ b/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, diff --git a/src/arch/sparc64/CodeGen.zig b/src/arch/sparc64/CodeGen.zig index b35f45dd64..6da576bafc 100644 --- a/src/arch/sparc64/CodeGen.zig +++ b/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"), diff --git a/src/arch/wasm/CodeGen.zig b/src/arch/wasm/CodeGen.zig index 2936025a49..6719f22a73 100644 --- a/src/arch/wasm/CodeGen.zig +++ b/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, diff --git a/src/arch/x86_64/CodeGen.zig b/src/arch/x86_64/CodeGen.zig index c652d48f3e..49e61e7067 100644 --- a/src/arch/x86_64/CodeGen.zig +++ b/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}); diff --git a/src/codegen/c.zig b/src/codegen/c.zig index 0fe3f80919..110c5a3127 100644 --- a/src/codegen/c.zig +++ b/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", .{}), diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 658764ba3c..b4950179c4 100644 --- a/src/codegen/llvm.zig +++ b/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), diff --git a/src/codegen/spirv.zig b/src/codegen/spirv.zig index b9eb56dd23..5170fa7428 100644 --- a/src/codegen/spirv.zig +++ b/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, diff --git a/test/behavior/cast.zig b/test/behavior/cast.zig index 9152efe7a7..3884cc734c 100644 --- a/test/behavior/cast.zig +++ b/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(); +} diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - i0 max.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - i0 max.zig new file mode 100644 index 0000000000..38ec595b45 --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - i0 min.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - i0 min.zig new file mode 100644 index 0000000000..97a651855b --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - signed max.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - signed max.zig new file mode 100644 index 0000000000..cc19ee84ff --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - signed min.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - signed min.zig new file mode 100644 index 0000000000..abc95e396a --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - u0 max.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - u0 max.zig new file mode 100644 index 0000000000..f488d0291f --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - u0 min.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - u0 min.zig new file mode 100644 index 0000000000..8d459e1a5c --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned max.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned max.zig new file mode 100644 index 0000000000..95122abc8c --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned min.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned min.zig new file mode 100644 index 0000000000..fbc7cf18ac --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - vector max.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - vector max.zig new file mode 100644 index 0000000000..35b4c91509 --- /dev/null +++ b/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 diff --git a/test/cases/safety/@intFromFloat cannot fit - boundary case - vector min.zig b/test/cases/safety/@intFromFloat cannot fit - boundary case - vector min.zig new file mode 100644 index 0000000000..94ad509772 --- /dev/null +++ b/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