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 74f4c5742486ce99a47f56b1ee55e57197748973..561ab86405566029624e6d52beb05884e2b973dc 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 c0b0d9b76cca3f4927dee804064b91094a83eab7..dafb3e5ed789c0d67ad82da678af43f06102647d 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 94efaf114c53ecb1382815b164ad7f59603cb206..94ed60fbf2f5eedd011e9e9be9c54812a2237b7e 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 2fafc44ab7ae38a88073a054365e05f29b84b336..85345ceb66831bcf735b2d075b91b652096b2aaa 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 7f5f396cae148cdbf2c7e4588f541efaf07d919c..53efa723562a78f857dfede9dde1ca6719f8b89b 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 1def679794a7db31e7ea14474aa3e583ed6bba85..2bf1ef108fe590a8dcde7c06ddc432b99912ea01 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 953cdbf2aca36190f196754572071da6a285d97a..8308a17e95dc52ec1e384079730d459c8e2ccdd9 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 4aaf6bf85c32c7040235ad63f4ad6d20652b5df7..a8a60e7ce724ac1e33f65830e95b517488153ec4 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 09304bf1def99dc837ca98faff48c85ea4b5fce4..98c142086308eaca1bc38d574770b5ed486eed41 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 c82061a5ddc877519c3be792013dbf2bdb06bc41..1743f37e8433b40af472d3f419cead5262fd1e52 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 b35f45dd6456702b876935cedcf06956b0573d06..6da576bafc7097302e84d7a628a13cdc2bb64443 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 2936025a4997f26e78dda315426e6563f31a8e55..6719f22a7339363361e4f6cdb93c4ee4e2486961 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 c652d48f3e12782087d1eed0c1461193da133670..49e61e70670ea69761f9f7ac183ccd7446326a49 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 0fe3f80919b6dc7766e747132f3ef166b6fb7956..110c5a312702d2d9786f746c5eeffaa48050cc9f 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 658764ba3cf7b81ba2132ac873671660262cff38..b4950179c4e640faa4a94b067f16619fe5cce1c9 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 b9eb56dd23761a37343fd2f9eae71a4d36c1aef5..5170fa7428435349d90aa42a6c6d0052da0ad2aa 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 9152efe7a700526836a70d8eba5bab7d043e2dcf..3884cc734c3b748424cd1aacd05c63c176b33753 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 0000000000000000000000000000000000000000..38ec595b454eab9f1b5daf9df7803c3794650f72 --- /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 0000000000000000000000000000000000000000..97a651855bcba1c3f34790775fb78cc52ba6ca9c --- /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 0000000000000000000000000000000000000000..cc19ee84ff4ce1c8f7d8b555ac2a25716795137f --- /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 0000000000000000000000000000000000000000..abc95e396a7966a428b2376b32766c5711663eed --- /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 0000000000000000000000000000000000000000..f488d0291f1b78a272b95fab12b5d7a904456e78 --- /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 0000000000000000000000000000000000000000..8d459e1a5ca42750e91e5c70e3f94ba8eaa2878c --- /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 0000000000000000000000000000000000000000..95122abc8c1574987c64c0c89e0f530018052acb --- /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 0000000000000000000000000000000000000000..fbc7cf18ac27d4588626e6c6f9911faa2e13c5e0 --- /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 0000000000000000000000000000000000000000..35b4c91509421460fe6b23c6f8288e276f4a0322 --- /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 0000000000000000000000000000000000000000..94ad5097728307141e8232d91ae140560764afb3 --- /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 -- 2.54.0