authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-06-15 14:18:37+01:00
committergravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2025-06-15 14:15:18-04:00
log6ffa285fc35651cb7f4b738a0f6e84718b821fd8
tree5c1307e2cfc9d6becd2bbcbdcd64979ca5dab5d9
parent6b41beb370ce62b4381396e0543e6eb7f70b18cf

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

27 files changed, 463 insertions(+), 32 deletions(-)

src/Air.zig+8
......@@ -683,6 +683,10 @@ pub const Inst = struct {
683683 int_from_float,
684684 /// Same as `int_from_float` with optimized float mode.
685685 int_from_float_optimized,
686 /// Same as `int_from_float`, but with a safety check that the operand is in bounds.
687 int_from_float_safe,
688 /// Same as `int_from_float_optimized`, but with a safety check that the operand is in bounds.
689 int_from_float_optimized_safe,
686690 /// Given an integer operand, return the float with the closest mathematical meaning.
687691 /// Uses the `ty_op` field.
688692 float_from_int,
......@@ -1612,6 +1616,8 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
16121616 .array_to_slice,
16131617 .int_from_float,
16141618 .int_from_float_optimized,
1619 .int_from_float_safe,
1620 .int_from_float_optimized_safe,
16151621 .float_from_int,
16161622 .splat,
16171623 .get_union_tag,
......@@ -1842,6 +1848,8 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
18421848 .sub_safe,
18431849 .mul_safe,
18441850 .intcast_safe,
1851 .int_from_float_safe,
1852 .int_from_float_optimized_safe,
18451853 => true,
18461854
18471855 .add,
src/Air/Legalize.zig+170
......@@ -112,6 +112,8 @@ pub const Feature = enum {
112112 scalarize_trunc,
113113 scalarize_int_from_float,
114114 scalarize_int_from_float_optimized,
115 scalarize_int_from_float_safe,
116 scalarize_int_from_float_optimized_safe,
115117 scalarize_float_from_int,
116118 scalarize_shuffle_one,
117119 scalarize_shuffle_two,
......@@ -126,6 +128,12 @@ pub const Feature = enum {
126128 /// Replace `intcast_safe` with an explicit safety check which `call`s the panic function on failure.
127129 /// Not compatible with `scalarize_intcast_safe`.
128130 expand_intcast_safe,
131 /// Replace `int_from_float_safe` with an explicit safety check which `call`s the panic function on failure.
132 /// Not compatible with `scalarize_int_from_float_safe`.
133 expand_int_from_float_safe,
134 /// Replace `int_from_float_optimized_safe` with an explicit safety check which `call`s the panic function on failure.
135 /// Not compatible with `scalarize_int_from_float_optimized_safe`.
136 expand_int_from_float_optimized_safe,
129137 /// Replace `add_safe` with an explicit safety check which `call`s the panic function on failure.
130138 /// Not compatible with `scalarize_add_safe`.
131139 expand_add_safe,
......@@ -225,6 +233,8 @@ pub const Feature = enum {
225233 .trunc => .scalarize_trunc,
226234 .int_from_float => .scalarize_int_from_float,
227235 .int_from_float_optimized => .scalarize_int_from_float_optimized,
236 .int_from_float_safe => .scalarize_int_from_float_safe,
237 .int_from_float_optimized_safe => .scalarize_int_from_float_optimized_safe,
228238 .float_from_int => .scalarize_float_from_int,
229239 .shuffle_one => .scalarize_shuffle_one,
230240 .shuffle_two => .scalarize_shuffle_two,
......@@ -439,6 +449,20 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
439449 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
440450 if (ty_op.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .ty_op);
441451 },
452 .int_from_float_safe => if (l.features.has(.expand_int_from_float_safe)) {
453 assert(!l.features.has(.scalarize_int_from_float_safe));
454 continue :inst l.replaceInst(inst, .block, try l.safeIntFromFloatBlockPayload(inst, false));
455 } else if (l.features.has(.scalarize_int_from_float_safe)) {
456 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
457 if (ty_op.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .ty_op);
458 },
459 .int_from_float_optimized_safe => if (l.features.has(.expand_int_from_float_optimized_safe)) {
460 assert(!l.features.has(.scalarize_int_from_float_optimized_safe));
461 continue :inst l.replaceInst(inst, .block, try l.safeIntFromFloatBlockPayload(inst, true));
462 } else if (l.features.has(.scalarize_int_from_float_optimized_safe)) {
463 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
464 if (ty_op.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .ty_op);
465 },
442466 .block, .loop => {
443467 const ty_pl = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_pl;
444468 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
20012025 .payload = try l.addBlockBody(main_block.body()),
20022026 } };
20032027}
2028fn safeIntFromFloatBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, optimized: bool) Error!Air.Inst.Data {
2029 const pt = l.pt;
2030 const zcu = pt.zcu;
2031 const gpa = zcu.gpa;
2032 const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;
2033
2034 const operand_ref = ty_op.operand;
2035 const operand_ty = l.typeOf(operand_ref);
2036 const dest_ty = ty_op.ty.toType();
2037
2038 const is_vector = operand_ty.zigTypeTag(zcu) == .vector;
2039 const dest_scalar_ty = dest_ty.scalarType(zcu);
2040 const int_info = dest_scalar_ty.intInfo(zcu);
2041
2042 // We emit 9 instructions in the worst case.
2043 var inst_buf: [9]Air.Inst.Index = undefined;
2044 try l.air_instructions.ensureUnusedCapacity(zcu.gpa, inst_buf.len);
2045 var main_block: Block = .init(&inst_buf);
2046
2047 // This check is a bit annoying because of floating-point rounding and the fact that this
2048 // builtin truncates. We'll use a bigint for our calculations, because we need to construct
2049 // integers exceeding the bounds of the result integer type, and we need to convert it to a
2050 // float with a specific rounding mode to avoid errors.
2051 // Our bigint may exceed the twos complement limit by one, so add an extra limb.
2052 const limbs = try gpa.alloc(
2053 std.math.big.Limb,
2054 std.math.big.int.calcTwosCompLimbCount(int_info.bits) + 1,
2055 );
2056 defer gpa.free(limbs);
2057 var big: std.math.big.int.Mutable = .init(limbs, 0);
2058
2059 // Check if the operand is lower than `min_int` when truncated to an integer.
2060 big.setTwosCompIntLimit(.min, int_info.signedness, int_info.bits);
2061 const below_min_inst: Air.Inst.Index = if (!big.positive or big.eqlZero()) bad: {
2062 // `min_int <= 0`, so check for `x <= min_int - 1`.
2063 big.addScalar(big.toConst(), -1);
2064 // For `<=`, we must round the RHS down, so that this value is the first `x` which returns `true`.
2065 const limit_val = try floatFromBigIntVal(pt, is_vector, operand_ty, big.toConst(), .floor);
2066 break :bad try main_block.addCmp(l, .lte, operand_ref, Air.internedToRef(limit_val.toIntern()), .{
2067 .vector = is_vector,
2068 .optimized = optimized,
2069 });
2070 } else {
2071 // `min_int > 0`, which is currently impossible. It would become possible under #3806, in
2072 // which case we must detect `x < min_int`.
2073 unreachable;
2074 };
2075
2076 // Check if the operand is greater than `max_int` when truncated to an integer.
2077 big.setTwosCompIntLimit(.max, int_info.signedness, int_info.bits);
2078 const above_max_inst: Air.Inst.Index = if (big.positive or big.eqlZero()) bad: {
2079 // `max_int >= 0`, so check for `x >= max_int + 1`.
2080 big.addScalar(big.toConst(), 1);
2081 // For `>=`, we must round the RHS up, so that this value is the first `x` which returns `true`.
2082 const limit_val = try floatFromBigIntVal(pt, is_vector, operand_ty, big.toConst(), .ceil);
2083 break :bad try main_block.addCmp(l, .gte, operand_ref, Air.internedToRef(limit_val.toIntern()), .{
2084 .vector = is_vector,
2085 .optimized = optimized,
2086 });
2087 } else {
2088 // `max_int < 0`, which is currently impossible. It would become possible under #3806, in
2089 // which case we must detect `x > max_int`.
2090 unreachable;
2091 };
2092
2093 // Combine the conditions.
2094 const out_of_bounds_inst: Air.Inst.Index = main_block.add(l, .{
2095 .tag = .bool_or,
2096 .data = .{ .bin_op = .{
2097 .lhs = below_min_inst.toRef(),
2098 .rhs = above_max_inst.toRef(),
2099 } },
2100 });
2101 const scalar_out_of_bounds_inst: Air.Inst.Index = if (is_vector) main_block.add(l, .{
2102 .tag = .reduce,
2103 .data = .{ .reduce = .{
2104 .operand = out_of_bounds_inst.toRef(),
2105 .operation = .Or,
2106 } },
2107 }) else out_of_bounds_inst;
2108
2109 // Now emit the actual condbr. "true" will be safety panic. "false" will be "ok", meaning we do
2110 // the `int_from_float` and `br` the result to `orig_inst`.
2111 var condbr: CondBr = .init(l, scalar_out_of_bounds_inst.toRef(), &main_block, .{ .true = .cold });
2112 condbr.then_block = .init(main_block.stealRemainingCapacity());
2113 try condbr.then_block.addPanic(l, .integer_part_out_of_bounds);
2114 condbr.else_block = .init(condbr.then_block.stealRemainingCapacity());
2115 const cast_inst = condbr.else_block.add(l, .{
2116 .tag = if (optimized) .int_from_float_optimized else .int_from_float,
2117 .data = .{ .ty_op = .{
2118 .ty = Air.internedToRef(dest_ty.toIntern()),
2119 .operand = operand_ref,
2120 } },
2121 });
2122 _ = condbr.else_block.add(l, .{
2123 .tag = .br,
2124 .data = .{ .br = .{
2125 .block_inst = orig_inst,
2126 .operand = cast_inst.toRef(),
2127 } },
2128 });
2129 _ = condbr.else_block.stealRemainingCapacity(); // we might not have used it all
2130 try condbr.finish(l);
2131
2132 return .{ .ty_pl = .{
2133 .ty = Air.internedToRef(dest_ty.toIntern()),
2134 .payload = try l.addBlockBody(main_block.body()),
2135 } };
2136}
20042137fn safeArithmeticBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, overflow_op_tag: Air.Inst.Tag) Error!Air.Inst.Data {
20052138 const pt = l.pt;
20062139 const zcu = pt.zcu;
......@@ -2378,6 +2511,42 @@ fn packedAggregateInitBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Erro
23782511 } };
23792512}
23802513
2514/// Given a `std.math.big.int.Const`, converts it to a `Value` which is a float of type `float_ty`
2515/// representing the same numeric value. If the integer cannot be exactly represented, `round`
2516/// decides whether the value should be rounded up or down. If `is_vector`, then `float_ty` is
2517/// instead a vector of floats, and the result value is a vector containing the converted scalar
2518/// repeated N times.
2519fn floatFromBigIntVal(
2520 pt: Zcu.PerThread,
2521 is_vector: bool,
2522 float_ty: Type,
2523 x: std.math.big.int.Const,
2524 round: std.math.big.int.Round,
2525) Error!Value {
2526 const zcu = pt.zcu;
2527 const scalar_ty = switch (is_vector) {
2528 true => float_ty.childType(zcu),
2529 false => float_ty,
2530 };
2531 assert(scalar_ty.zigTypeTag(zcu) == .float);
2532 const scalar_val: Value = switch (scalar_ty.floatBits(zcu.getTarget())) {
2533 16 => try pt.floatValue(scalar_ty, x.toFloat(f16, round)[0]),
2534 32 => try pt.floatValue(scalar_ty, x.toFloat(f32, round)[0]),
2535 64 => try pt.floatValue(scalar_ty, x.toFloat(f64, round)[0]),
2536 80 => try pt.floatValue(scalar_ty, x.toFloat(f80, round)[0]),
2537 128 => try pt.floatValue(scalar_ty, x.toFloat(f128, round)[0]),
2538 else => unreachable,
2539 };
2540 if (is_vector) {
2541 return .fromInterned(try pt.intern(.{ .aggregate = .{
2542 .ty = float_ty.toIntern(),
2543 .storage = .{ .repeated_elem = scalar_val.toIntern() },
2544 } }));
2545 } else {
2546 return scalar_val;
2547 }
2548}
2549
23812550const Block = struct {
23822551 instructions: []Air.Inst.Index,
23832552 len: usize,
......@@ -2735,4 +2904,5 @@ const InternPool = @import("../InternPool.zig");
27352904const Legalize = @This();
27362905const std = @import("std");
27372906const Type = @import("../Type.zig");
2907const Value = @import("../Value.zig");
27382908const Zcu = @import("../Zcu.zig");
src/Air/Liveness.zig+4
......@@ -374,6 +374,8 @@ pub fn categorizeOperand(
374374 .array_to_slice,
375375 .int_from_float,
376376 .int_from_float_optimized,
377 .int_from_float_safe,
378 .int_from_float_optimized_safe,
377379 .float_from_int,
378380 .get_union_tag,
379381 .clz,
......@@ -1015,6 +1017,8 @@ fn analyzeInst(
10151017 .array_to_slice,
10161018 .int_from_float,
10171019 .int_from_float_optimized,
1020 .int_from_float_safe,
1021 .int_from_float_optimized_safe,
10181022 .float_from_int,
10191023 .get_union_tag,
10201024 .clz,
src/Air/Liveness/Verify.zig+2
......@@ -107,6 +107,8 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
107107 .array_to_slice,
108108 .int_from_float,
109109 .int_from_float_optimized,
110 .int_from_float_safe,
111 .int_from_float_optimized_safe,
110112 .float_from_int,
111113 .get_union_tag,
112114 .clz,
src/Air/print.zig+2
......@@ -250,6 +250,8 @@ const Writer = struct {
250250 .splat,
251251 .int_from_float,
252252 .int_from_float_optimized,
253 .int_from_float_safe,
254 .int_from_float_optimized_safe,
253255 .get_union_tag,
254256 .clz,
255257 .ctz,
src/Air/types_resolved.zig+2
......@@ -130,6 +130,8 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
130130 .array_to_slice,
131131 .int_from_float,
132132 .int_from_float_optimized,
133 .int_from_float_safe,
134 .int_from_float_optimized_safe,
133135 .float_from_int,
134136 .splat,
135137 .error_set_has_value,
src/Sema.zig+21-31
......@@ -22178,44 +22178,34 @@ fn zirIntFromFloat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro
2217822178
2217922179 try sema.requireRuntimeBlock(block, src, operand_src);
2218022180 if (dest_scalar_ty.intInfo(zcu).bits == 0) {
22181 if (!is_vector) {
22182 if (block.wantSafety()) {
22183 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()));
22184 try sema.addSafetyCheck(block, src, ok, .integer_part_out_of_bounds);
22185 }
22186 return Air.internedToRef((try pt.intValue(dest_ty, 0)).toIntern());
22187 }
2218822181 if (block.wantSafety()) {
22189 const len = dest_ty.vectorLen(zcu);
22190 for (0..len) |i| {
22191 const idx_ref = try pt.intRef(.usize, i);
22192 const elem_ref = try block.addBinOp(.array_elem_val, operand, idx_ref);
22193 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()));
22194 try sema.addSafetyCheck(block, src, ok, .integer_part_out_of_bounds);
22195 }
22196 }
22182 // Emit an explicit safety check. We can do this one like `abs(x) < 1`.
22183 const abs_ref = try block.addTyOp(.abs, operand_ty, operand);
22184 const max_abs_ref = if (is_vector) try block.addReduce(abs_ref, .Max) else abs_ref;
22185 const one_ref = Air.internedToRef((try pt.floatValue(operand_scalar_ty, 1.0)).toIntern());
22186 const ok_ref = try block.addBinOp(.cmp_lt, max_abs_ref, one_ref);
22187 try sema.addSafetyCheck(block, src, ok_ref, .integer_part_out_of_bounds);
22188 }
22189 const scalar_val = try pt.intValue(dest_scalar_ty, 0);
22190 if (!is_vector) return Air.internedToRef(scalar_val.toIntern());
2219722191 return Air.internedToRef(try pt.intern(.{ .aggregate = .{
2219822192 .ty = dest_ty.toIntern(),
22199 .storage = .{ .repeated_elem = (try pt.intValue(dest_scalar_ty, 0)).toIntern() },
22193 .storage = .{ .repeated_elem = scalar_val.toIntern() },
2220022194 } }));
2220122195 }
22202 const result = try block.addTyOp(if (block.float_mode == .optimized) .int_from_float_optimized else .int_from_float, dest_ty, operand);
2220322196 if (block.wantSafety()) {
22204 const back = try block.addTyOp(.float_from_int, operand_ty, result);
22205 const diff = try block.addBinOp(if (block.float_mode == .optimized) .sub_optimized else .sub, operand, back);
22206 const ok = if (is_vector) ok: {
22207 const ok_pos = try block.addCmpVector(diff, Air.internedToRef((try sema.splat(operand_ty, try pt.floatValue(operand_scalar_ty, 1.0))).toIntern()), .lt);
22208 const ok_neg = try block.addCmpVector(diff, Air.internedToRef((try sema.splat(operand_ty, try pt.floatValue(operand_scalar_ty, -1.0))).toIntern()), .gt);
22209 const ok = try block.addBinOp(.bit_and, ok_pos, ok_neg);
22210 break :ok try block.addReduce(ok, .And);
22211 } else ok: {
22212 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()));
22213 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()));
22214 break :ok try block.addBinOp(.bool_and, ok_pos, ok_neg);
22215 };
22216 try sema.addSafetyCheck(block, src, ok, .integer_part_out_of_bounds);
22197 if (zcu.backendSupportsFeature(.panic_fn)) {
22198 _ = try sema.preparePanicId(src, .integer_part_out_of_bounds);
22199 }
22200 return block.addTyOp(switch (block.float_mode) {
22201 .optimized => .int_from_float_optimized_safe,
22202 .strict => .int_from_float_safe,
22203 }, dest_ty, operand);
2221722204 }
22218 return result;
22205 return block.addTyOp(switch (block.float_mode) {
22206 .optimized => .int_from_float_optimized,
22207 .strict => .int_from_float,
22208 }, dest_ty, operand);
2221922209}
2222022210
2222122211fn zirFloatFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
src/arch/aarch64/CodeGen.zig+2
......@@ -861,6 +861,8 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
861861 .sub_safe,
862862 .mul_safe,
863863 .intcast_safe,
864 .int_from_float_safe,
865 .int_from_float_optimized_safe,
864866 => return self.fail("TODO implement safety_checked_instructions", .{}),
865867
866868 .is_named_enum_value => return self.fail("TODO implement is_named_enum_value", .{}),
src/arch/arm/CodeGen.zig+2
......@@ -850,6 +850,8 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
850850 .sub_safe,
851851 .mul_safe,
852852 .intcast_safe,
853 .int_from_float_safe,
854 .int_from_float_optimized_safe,
853855 => return self.fail("TODO implement safety_checked_instructions", .{}),
854856
855857 .is_named_enum_value => return self.fail("TODO implement is_named_enum_value", .{}),
src/arch/riscv64/CodeGen.zig+4
......@@ -54,6 +54,8 @@ const InnerError = CodeGenError || error{OutOfRegisters};
5454pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
5555 return comptime &.initMany(&.{
5656 .expand_intcast_safe,
57 .expand_int_from_float_safe,
58 .expand_int_from_float_optimized_safe,
5759 .expand_add_safe,
5860 .expand_sub_safe,
5961 .expand_mul_safe,
......@@ -1474,6 +1476,8 @@ fn genBody(func: *Func, body: []const Air.Inst.Index) InnerError!void {
14741476 .sub_safe,
14751477 .mul_safe,
14761478 .intcast_safe,
1479 .int_from_float_safe,
1480 .int_from_float_optimized_safe,
14771481 => return func.fail("TODO implement safety_checked_instructions", .{}),
14781482
14791483 .cmp_lt,
src/arch/sparc64/CodeGen.zig+2
......@@ -696,6 +696,8 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
696696 .sub_safe,
697697 .mul_safe,
698698 .intcast_safe,
699 .int_from_float_safe,
700 .int_from_float_optimized_safe,
699701 => @panic("TODO implement safety_checked_instructions"),
700702
701703 .is_named_enum_value => @panic("TODO implement is_named_enum_value"),
src/arch/wasm/CodeGen.zig+4
......@@ -31,6 +31,8 @@ const compilerRtIntAbbrev = target_util.compilerRtIntAbbrev;
3131pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
3232 return comptime &.initMany(&.{
3333 .expand_intcast_safe,
34 .expand_int_from_float_safe,
35 .expand_int_from_float_optimized_safe,
3436 .expand_add_safe,
3537 .expand_sub_safe,
3638 .expand_mul_safe,
......@@ -2020,6 +2022,8 @@ fn genInst(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void {
20202022 .sub_safe,
20212023 .mul_safe,
20222024 .intcast_safe,
2025 .int_from_float_safe,
2026 .int_from_float_optimized_safe,
20232027 => return cg.fail("TODO implement safety_checked_instructions", .{}),
20242028
20252029 .work_item_id,
src/arch/x86_64/CodeGen.zig+4
......@@ -102,6 +102,8 @@ pub fn legalizeFeatures(target: *const std.Target) *const Air.Legalize.Features
102102 .reduce_one_elem_to_bitcast = true,
103103
104104 .expand_intcast_safe = true,
105 .expand_int_from_float_safe = true,
106 .expand_int_from_float_optimized_safe = true,
105107 .expand_add_safe = true,
106108 .expand_sub_safe = true,
107109 .expand_mul_safe = true,
......@@ -107763,6 +107765,8 @@ fn genBody(cg: *CodeGen, body: []const Air.Inst.Index) InnerError!void {
107763107765 };
107764107766 try res[0].finish(inst, &.{ty_op.operand}, &ops, cg);
107765107767 },
107768 .int_from_float_safe => unreachable,
107769 .int_from_float_optimized_safe => unreachable,
107766107770 .float_from_int => |air_tag| if (use_old) try cg.airFloatFromInt(inst) else {
107767107771 const ty_op = air_datas[@intFromEnum(inst)].ty_op;
107768107772 var ops = try cg.tempsFromOperands(inst, .{ty_op.operand});
src/codegen/c.zig+4
......@@ -27,6 +27,8 @@ pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features {
2727 inline false, true => |supports_legalize| &.init(.{
2828 // we don't currently ask zig1 to use safe optimization modes
2929 .expand_intcast_safe = supports_legalize,
30 .expand_int_from_float_safe = supports_legalize,
31 .expand_int_from_float_optimized_safe = supports_legalize,
3032 .expand_add_safe = supports_legalize,
3133 .expand_sub_safe = supports_legalize,
3234 .expand_mul_safe = supports_legalize,
......@@ -3578,6 +3580,8 @@ fn genBodyInner(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail,
35783580 .sub_safe,
35793581 .mul_safe,
35803582 .intcast_safe,
3583 .int_from_float_safe,
3584 .int_from_float_optimized_safe,
35813585 => return f.fail("TODO implement safety_checked_instructions", .{}),
35823586
35833587 .is_named_enum_value => return f.fail("TODO: C backend: implement is_named_enum_value", .{}),
src/codegen/llvm.zig+6-1
......@@ -37,7 +37,10 @@ const compilerRtIntAbbrev = target_util.compilerRtIntAbbrev;
3737const Error = error{ OutOfMemory, CodegenFail };
3838
3939pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features {
40 return null;
40 return comptime &.initMany(&.{
41 .expand_int_from_float_safe,
42 .expand_int_from_float_optimized_safe,
43 });
4144}
4245
4346fn subArchName(target: std.Target, comptime family: std.Target.Cpu.Arch.Family, mappings: anytype) ?[]const u8 {
......@@ -4987,6 +4990,8 @@ pub const FuncGen = struct {
49874990
49884991 .int_from_float => try self.airIntFromFloat(inst, .normal),
49894992 .int_from_float_optimized => try self.airIntFromFloat(inst, .fast),
4993 .int_from_float_safe => unreachable, // handled by `legalizeFeatures`
4994 .int_from_float_optimized_safe => unreachable, // handled by `legalizeFeatures`
49904995
49914996 .array_to_slice => try self.airArrayToSlice(inst),
49924997 .float_from_int => try self.airFloatFromInt(inst),
src/codegen/spirv.zig+2
......@@ -31,6 +31,8 @@ const InstMap = std.AutoHashMapUnmanaged(Air.Inst.Index, IdRef);
3131pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
3232 return comptime &.initMany(&.{
3333 .expand_intcast_safe,
34 .expand_int_from_float_safe,
35 .expand_int_from_float_optimized_safe,
3436 .expand_add_safe,
3537 .expand_sub_safe,
3638 .expand_mul_safe,
test/behavior/cast.zig+64
......@@ -102,6 +102,7 @@ test "comptime_int @floatFromInt" {
102102test "@floatFromInt" {
103103 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
104104 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
105 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
105106 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
106107
107108 const S = struct {
......@@ -2737,3 +2738,66 @@ test "peer type resolution: slice of sentinel-terminated array" {
27372738 try expect(result[0][0] == 10);
27382739 try expect(result[0][1] == 20);
27392740}
2741
2742test "@intFromFloat boundary cases" {
2743 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
2744
2745 const S = struct {
2746 fn case(comptime I: type, x: f32, bump: enum { up, down }, expected: I) !void {
2747 const input: f32 = switch (bump) {
2748 .up => std.math.nextAfter(f32, x, std.math.inf(f32)),
2749 .down => std.math.nextAfter(f32, x, -std.math.inf(f32)),
2750 };
2751 const output: I = @intFromFloat(input);
2752 try expect(output == expected);
2753 }
2754 fn doTheTest() !void {
2755 try case(u8, 256.0, .down, 255);
2756 try case(u8, -1.0, .up, 0);
2757 try case(i8, 128.0, .down, 127);
2758 try case(i8, -129.0, .up, -128);
2759
2760 try case(u0, 1.0, .down, 0);
2761 try case(u0, -1.0, .up, 0);
2762 try case(i0, 1.0, .down, 0);
2763 try case(i0, -1.0, .up, 0);
2764
2765 try case(u10, 1024.0, .down, 1023);
2766 try case(u10, -1.0, .up, 0);
2767 try case(i10, 512.0, .down, 511);
2768 try case(i10, -513.0, .up, -512);
2769 }
2770 };
2771 try S.doTheTest();
2772 try comptime S.doTheTest();
2773}
2774
2775test "@intFromFloat vector boundary cases" {
2776 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
2777 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest;
2778 if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
2779
2780 const S = struct {
2781 fn case(comptime I: type, unshifted_inputs: [2]f32, expected: [2]I) !void {
2782 const inputs: @Vector(2, f32) = .{
2783 std.math.nextAfter(f32, unshifted_inputs[0], std.math.inf(f32)),
2784 std.math.nextAfter(f32, unshifted_inputs[1], -std.math.inf(f32)),
2785 };
2786 const outputs: @Vector(2, I) = @intFromFloat(inputs);
2787 try expect(outputs[0] == expected[0]);
2788 try expect(outputs[1] == expected[1]);
2789 }
2790 fn doTheTest() !void {
2791 try case(u8, .{ -1.0, 256.0 }, .{ 0, 255 });
2792 try case(i8, .{ -129.0, 128.0 }, .{ -128, 127 });
2793
2794 try case(u0, .{ -1.0, 1.0 }, .{ 0, 0 });
2795 try case(i0, .{ -1.0, 1.0 }, .{ 0, 0 });
2796
2797 try case(u10, .{ -1.0, 1024.0 }, .{ 0, 1023 });
2798 try case(i10, .{ -513.0, 512.0 }, .{ -512, 511 });
2799 }
2800 };
2801 try S.doTheTest();
2802 try comptime S.doTheTest();
2803}
test/cases/safety/@intFromFloat cannot fit - boundary case - i0 max.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = 1.0;
10pub fn main() !void {
11 _ = @as(i0, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - i0 min.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = -1.0;
10pub fn main() !void {
11 _ = @as(i0, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - signed max.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = 128;
10pub fn main() !void {
11 _ = @as(i8, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - signed min.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = -129;
10pub fn main() !void {
11 _ = @as(i8, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - u0 max.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = 1.0;
10pub fn main() !void {
11 _ = @as(u0, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - u0 min.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = -1.0;
10pub fn main() !void {
11 _ = @as(u0, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned max.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = 256;
10pub fn main() !void {
11 _ = @as(u8, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - unsigned min.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: f32 = -1;
10pub fn main() !void {
11 _ = @as(u8, @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - vector max.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: @Vector(2, f32) = .{ 100, 512 };
10pub fn main() !void {
11 _ = @as(@Vector(2, i10), @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native
test/cases/safety/@intFromFloat cannot fit - boundary case - vector min.zig created+16
......@@ -0,0 +1,16 @@
1const std = @import("std");
2pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
3 _ = stack_trace;
4 if (std.mem.eql(u8, message, "integer part of floating point value out of bounds")) {
5 std.process.exit(0);
6 }
7 std.process.exit(1);
8}
9var x: @Vector(2, f32) = .{ 100, -513 };
10pub fn main() !void {
11 _ = @as(@Vector(2, i10), @intFromFloat(x));
12 return error.TestFailed;
13}
14// run
15// backend=stage2,llvm
16// target=native