diff --git a/lib/compiler/aro/aro/Value.zig b/lib/compiler/aro/aro/Value.zig index f736d63adf..02c9cfc830 100644 --- a/lib/compiler/aro/aro/Value.zig +++ b/lib/compiler/aro/aro/Value.zig @@ -148,35 +148,25 @@ pub fn floatToInt(v: *Value, dest_ty: Type, comp: *Compilation) !FloatToIntChang return .out_of_range; } - const had_fraction = @rem(float_val, 1) != 0; - const is_negative = std.math.signbit(float_val); - const floored = @floor(@abs(float_val)); - - var rational = try std.math.big.Rational.init(comp.gpa); - defer rational.deinit(); - rational.setFloat(f128, floored) catch |err| switch (err) { - error.NonFiniteFloat => { - v.* = .{}; - return .overflow; - }, - error.OutOfMemory => return error.OutOfMemory, - }; - - // The float is reduced in rational.setFloat, so we assert that denominator is equal to one - const big_one = BigIntConst{ .limbs = &.{1}, .positive = true }; - assert(rational.q.toConst().eqlAbs(big_one)); - - if (is_negative) { - rational.negate(); - } - const signedness = dest_ty.signedness(comp); const bits: usize = @intCast(dest_ty.bitSizeof(comp).?); - // rational.p.truncate(rational.p.toConst(), signedness: Signedness, bit_count: usize) - const fits = rational.p.fitsInTwosComp(signedness, bits); - v.* = try intern(comp, .{ .int = .{ .big_int = rational.p.toConst() } }); - try rational.p.truncate(&rational.p, signedness, bits); + var big_int: std.math.big.int.Mutable = .{ + .limbs = try comp.gpa.alloc(std.math.big.Limb, @max( + std.math.big.int.calcLimbLen(float_val), + std.math.big.int.calcTwosCompLimbCount(bits), + )), + .len = undefined, + .positive = undefined, + }; + const had_fraction = switch (big_int.setFloat(float_val, .trunc)) { + .inexact => true, + .exact => false, + }; + + const fits = big_int.toConst().fitsInTwosComp(signedness, bits); + v.* = try intern(comp, .{ .int = .{ .big_int = big_int.toConst() } }); + big_int.truncate(big_int.toConst(), signedness, bits); if (!was_zero and v.isZero(comp)) return .nonzero_to_zero; if (!fits) return .out_of_range; diff --git a/lib/std/math.zig b/lib/std/math.zig index 5ffa7f0509..1cd9a83a14 100644 --- a/lib/std/math.zig +++ b/lib/std/math.zig @@ -45,6 +45,7 @@ pub const rad_per_deg = 0.017453292519943295769236907684886127134428718885417254 /// 180.0/pi pub const deg_per_rad = 57.295779513082320876798154814105170332405472466564321549160243861; +pub const FloatRepr = float.FloatRepr; pub const floatExponentBits = float.floatExponentBits; pub const floatMantissaBits = float.floatMantissaBits; pub const floatFractionalBits = float.floatFractionalBits; diff --git a/lib/std/math/big.zig b/lib/std/math/big.zig index c201bf1df2..37f40587c5 100644 --- a/lib/std/math/big.zig +++ b/lib/std/math/big.zig @@ -1,7 +1,6 @@ const std = @import("../std.zig"); const assert = std.debug.assert; -pub const Rational = @import("big/rational.zig").Rational; pub const int = @import("big/int.zig"); pub const Limb = usize; const limb_info = @typeInfo(Limb).int; @@ -18,7 +17,6 @@ comptime { test { _ = int; - _ = Rational; _ = Limb; _ = SignedLimb; _ = DoubleLimb; diff --git a/lib/std/math/big/int.zig b/lib/std/math/big/int.zig index 485050b1ab..552ded4d51 100644 --- a/lib/std/math/big/int.zig +++ b/lib/std/math/big/int.zig @@ -18,17 +18,28 @@ const Signedness = std.builtin.Signedness; const native_endian = builtin.cpu.arch.endian(); /// Returns the number of limbs needed to store `scalar`, which must be a -/// primitive integer value. +/// primitive integer or float value. /// Note: A comptime-known upper bound of this value that may be used /// instead if `scalar` is not already comptime-known is /// `calcTwosCompLimbCount(@typeInfo(@TypeOf(scalar)).int.bits)` pub fn calcLimbLen(scalar: anytype) usize { - if (scalar == 0) { - return 1; + switch (@typeInfo(@TypeOf(scalar))) { + .int, .comptime_int => { + if (scalar == 0) return 1; + const w_value = @abs(scalar); + return @as(usize, @intCast(@divFloor(@as(Limb, @intCast(math.log2(w_value))), limb_bits) + 1)); + }, + .float => { + const repr: std.math.FloatRepr(@TypeOf(scalar)) = @bitCast(scalar); + return switch (repr.exponent) { + .denormal => 1, + else => return calcNonZeroTwosCompLimbCount(@as(usize, 2) + @max(repr.exponent.unbias(), 0)), + .infinite => 0, + }; + }, + .comptime_float => return calcLimbLen(@as(f128, scalar)), + else => @compileError("expected float or int, got " ++ @typeName(@TypeOf(scalar))), } - - const w_value = @abs(scalar); - return @as(usize, @intCast(@divFloor(@as(Limb, @intCast(math.log2(w_value))), limb_bits) + 1)); } pub fn calcToStringLimbsBufferLen(a_len: usize, base: u8) usize { @@ -134,6 +145,22 @@ pub const TwosCompIntLimit = enum { max, }; +pub const Round = enum { + /// Round to the nearest representable value, with ties broken by the representation + /// that ends with a 0 bit. + nearest_even, + /// Round away from zero. + away, + /// Round towards zero. + trunc, + /// Round towards negative infinity. + floor, + /// Round towards positive infinity. + ceil, +}; + +pub const Exactness = enum { inexact, exact }; + /// A arbitrary-precision big integer, with a fixed set of mutable limbs. pub const Mutable = struct { /// Raw digits. These are: @@ -155,6 +182,20 @@ pub const Mutable = struct { }; } + pub const ConvertError = Const.ConvertError; + + /// Convert `self` to `Int`. + /// + /// Returns an error if self cannot be narrowed into the requested type without truncation. + pub fn toInt(self: Mutable, comptime Int: type) ConvertError!Int { + return self.toConst().toInt(Int); + } + + /// Convert `self` to `Float`. + pub fn toFloat(self: Mutable, comptime Float: type, round: Round) struct { Float, Exactness } { + return self.toConst().toFloat(Float, round); + } + /// Returns true if `a == 0`. pub fn eqlZero(self: Mutable) bool { return self.toConst().eqlZero(); @@ -401,6 +442,65 @@ pub const Mutable = struct { } } + /// Sets the Mutable to a float value rounded according to `round`. + /// Returns whether the conversion was exact (`round` had no effect on the result). + pub fn setFloat(self: *Mutable, value: anytype, round: Round) Exactness { + const Float = @TypeOf(value); + if (Float == comptime_float) return self.setFloat(@as(f128, value), round); + const abs_value = @abs(value); + if (abs_value < 1.0) { + if (abs_value == 0.0) { + self.set(0); + return .exact; + } + self.set(@as(i2, round: switch (round) { + .nearest_even => if (abs_value <= 0.5) 0 else continue :round .away, + .away => if (value < 0.0) -1 else 1, + .trunc => 0, + .floor => -@as(i2, @intFromBool(value < 0.0)), + .ceil => @intFromBool(value > 0.0), + })); + return .inexact; + } + const Repr = std.math.FloatRepr(Float); + const repr: Repr = @bitCast(value); + const exponent = repr.exponent.unbias(); + assert(exponent >= 0); + const int_bit: Repr.Mantissa = 1 << (@bitSizeOf(Repr.Mantissa) - 1); + const mantissa = int_bit | repr.mantissa; + if (exponent >= @bitSizeOf(Repr.Normalized.Fraction)) { + self.set(mantissa); + self.shiftLeft(self.toConst(), @intCast(exponent - @bitSizeOf(Repr.Normalized.Fraction))); + self.positive = repr.sign == .positive; + return .exact; + } + self.set(mantissa >> @intCast(@bitSizeOf(Repr.Normalized.Fraction) - exponent)); + const round_bits: Repr.Normalized.Fraction = @truncate(mantissa << @intCast(exponent)); + if (round_bits == 0) { + self.positive = repr.sign == .positive; + return .exact; + } + round: switch (round) { + .nearest_even => { + const half: Repr.Normalized.Fraction = 1 << (@bitSizeOf(Repr.Normalized.Fraction) - 1); + if (round_bits >= half) self.addScalar(self.toConst(), 1); + if (round_bits == half) self.limbs[0] &= ~@as(Limb, 1); + }, + .away => self.addScalar(self.toConst(), 1), + .trunc => {}, + .floor => switch (repr.sign) { + .positive => {}, + .negative => continue :round .away, + }, + .ceil => switch (repr.sign) { + .positive => continue :round .away, + .negative => {}, + }, + } + self.positive = repr.sign == .positive; + return .inexact; + } + /// r = a + scalar /// /// r and a may be aliases. @@ -2117,25 +2217,25 @@ pub const Const = struct { /// Deprecated; use `toInt`. pub const to = toInt; - /// Convert self to integer type T. + /// Convert `self` to `Int`. /// /// Returns an error if self cannot be narrowed into the requested type without truncation. - pub fn toInt(self: Const, comptime T: type) ConvertError!T { - switch (@typeInfo(T)) { + pub fn toInt(self: Const, comptime Int: type) ConvertError!Int { + switch (@typeInfo(Int)) { .int => |info| { // Make sure -0 is handled correctly. if (self.eqlZero()) return 0; - const UT = std.meta.Int(.unsigned, info.bits); + const Unsigned = std.meta.Int(.unsigned, info.bits); if (!self.fitsInTwosComp(info.signedness, info.bits)) { return error.TargetTooSmall; } - var r: UT = 0; + var r: Unsigned = 0; - if (@sizeOf(UT) <= @sizeOf(Limb)) { - r = @as(UT, @intCast(self.limbs[0])); + if (@sizeOf(Unsigned) <= @sizeOf(Limb)) { + r = @intCast(self.limbs[0]); } else { for (self.limbs[0..self.limbs.len], 0..) |_, ri| { const limb = self.limbs[self.limbs.len - ri - 1]; @@ -2145,40 +2245,76 @@ pub const Const = struct { } if (info.signedness == .unsigned) { - return if (self.positive) @as(T, @intCast(r)) else error.NegativeIntoUnsigned; + return if (self.positive) @intCast(r) else error.NegativeIntoUnsigned; } else { if (self.positive) { return @intCast(r); } else { - if (math.cast(T, r)) |ok| { + if (math.cast(Int, r)) |ok| { return -ok; } else { - return minInt(T); + return minInt(Int); } } } }, - else => @compileError("expected int type, found '" ++ @typeName(T) ++ "'"), + else => @compileError("expected int type, found '" ++ @typeName(Int) ++ "'"), } } - /// Convert self to float type T. - pub fn toFloat(self: Const, comptime T: type) T { - if (self.limbs.len == 0) return 0; + /// Convert self to `Float`. + pub fn toFloat(self: Const, comptime Float: type, round: Round) struct { Float, Exactness } { + if (Float == comptime_float) return self.toFloat(f128, round); + const normalized_abs: Const = .{ + .limbs = self.limbs[0..llnormalize(self.limbs)], + .positive = true, + }; + if (normalized_abs.eqlZero()) return .{ if (self.positive) 0.0 else -0.0, .exact }; - const base = std.math.maxInt(std.math.big.Limb) + 1; - var result: f128 = 0; - var i: usize = self.limbs.len; - while (i != 0) { - i -= 1; - const limb: f128 = @floatFromInt(self.limbs[i]); - result = @mulAdd(f128, base, result, limb); - } - if (self.positive) { - return @floatCast(result); - } else { - return @floatCast(-result); - } + const Repr = std.math.FloatRepr(Float); + var mantissa_limbs: [calcNonZeroTwosCompLimbCount(1 + @bitSizeOf(Repr.Mantissa))]Limb = undefined; + var mantissa: Mutable = .{ + .limbs = &mantissa_limbs, + .positive = undefined, + .len = undefined, + }; + var exponent = normalized_abs.bitCountAbs() - 1; + const exactness: Exactness = exactness: { + if (exponent <= @bitSizeOf(Repr.Normalized.Fraction)) { + mantissa.shiftLeft(normalized_abs, @intCast(@bitSizeOf(Repr.Normalized.Fraction) - exponent)); + break :exactness .exact; + } + const shift: usize = @intCast(exponent - @bitSizeOf(Repr.Normalized.Fraction)); + mantissa.shiftRight(normalized_abs, shift); + const final_limb_index = (shift - 1) / limb_bits; + const round_bits = normalized_abs.limbs[final_limb_index] << @truncate(-%shift) | + @intFromBool(!std.mem.allEqual(Limb, normalized_abs.limbs[0..final_limb_index], 0)); + if (round_bits == 0) break :exactness .exact; + round: switch (round) { + .nearest_even => { + const half: Limb = 1 << (limb_bits - 1); + if (round_bits >= half) mantissa.addScalar(mantissa.toConst(), 1); + if (round_bits == half) mantissa.limbs[0] &= ~@as(Limb, 1); + }, + .away => mantissa.addScalar(mantissa.toConst(), 1), + .trunc => {}, + .floor => if (!self.positive) continue :round .away, + .ceil => if (self.positive) continue :round .away, + } + break :exactness .inexact; + }; + const normalized_res: Repr.Normalized = .{ + .fraction = @truncate(mantissa.toInt(Repr.Mantissa) catch |err| switch (err) { + error.NegativeIntoUnsigned => unreachable, + error.TargetTooSmall => fraction: { + assert(mantissa.toConst().orderAgainstScalar(1 << @bitSizeOf(Repr.Mantissa)).compare(.eq)); + exponent += 1; + break :fraction 1 << (@bitSizeOf(Repr.Mantissa) - 1); + }, + }), + .exponent = std.math.lossyCast(Repr.Normalized.Exponent, exponent), + }; + return .{ normalized_res.reconstruct(if (self.positive) .positive else .negative), exactness }; } /// To allow `std.fmt.format` to work with this type. @@ -2739,16 +2875,16 @@ pub const Managed = struct { /// Deprecated; use `toInt`. pub const to = toInt; - /// Convert self to integer type T. + /// Convert `self` to `Int`. /// /// Returns an error if self cannot be narrowed into the requested type without truncation. - pub fn toInt(self: Managed, comptime T: type) ConvertError!T { - return self.toConst().toInt(T); + pub fn toInt(self: Managed, comptime Int: type) ConvertError!Int { + return self.toConst().toInt(Int); } - /// Convert self to float type T. - pub fn toFloat(self: Managed, comptime T: type) T { - return self.toConst().toFloat(T); + /// Convert `self` to `Float`. + pub fn toFloat(self: Managed, comptime Float: type, round: Round) struct { Float, Exactness } { + return self.toConst().toFloat(Float, round); } /// Set self from the string representation `value`. @@ -3807,7 +3943,7 @@ fn llshr(r: []Limb, a: []const Limb, shift: usize) usize { // if the most significant limb becomes 0 after the shift const shrink = a[a.len - 1] >> bit_shift == 0; - std.debug.assert(r.len >= a.len - @intFromBool(!shrink)); + std.debug.assert(r.len >= a.len - @intFromBool(shrink)); var i: usize = 0; while (i < a.len - 1) : (i += 1) { @@ -4240,7 +4376,7 @@ test { const testing_allocator = std.testing.allocator; test "llshl shift by whole number of limb" { - const padding = std.math.maxInt(Limb); + const padding = maxInt(Limb); var r: [10]Limb = @splat(padding); @@ -4390,8 +4526,8 @@ test "llshr to 0" { try testOneShiftCase(.llshr, .{1, &.{0}, &.{1}}); try testOneShiftCase(.llshr, .{5, &.{0}, &.{1}}); try testOneShiftCase(.llshr, .{65, &.{0}, &.{0, 1}}); - try testOneShiftCase(.llshr, .{193, &.{0}, &.{0, 0, std.math.maxInt(Limb)}}); - try testOneShiftCase(.llshr, .{193, &.{0}, &.{std.math.maxInt(Limb), 1, std.math.maxInt(Limb)}}); + try testOneShiftCase(.llshr, .{193, &.{0}, &.{0, 0, maxInt(Limb)}}); + try testOneShiftCase(.llshr, .{193, &.{0}, &.{maxInt(Limb), 1, maxInt(Limb)}}); try testOneShiftCase(.llshr, .{193, &.{0}, &.{0xdeadbeef, 0xabcdefab, 0x1234}}); // zig fmt: on } @@ -4475,7 +4611,7 @@ fn testOneShiftCase(comptime function: enum { llshr, llshl }, case: Case) !void } fn testOneShiftCaseNoAliasing(func: fn ([]Limb, []const Limb, usize) usize, case: Case) !void { - const padding = std.math.maxInt(Limb); + const padding = maxInt(Limb); var r: [20]Limb = @splat(padding); const shift = case[0]; @@ -4492,7 +4628,7 @@ fn testOneShiftCaseNoAliasing(func: fn ([]Limb, []const Limb, usize) usize, case } fn testOneShiftCaseAliasing(func: fn ([]Limb, []const Limb, usize) usize, case: Case, shift_direction: isize) !void { - const padding = std.math.maxInt(Limb); + const padding = maxInt(Limb); var r: [60]Limb = @splat(padding); const base = 20; diff --git a/lib/std/math/big/int_test.zig b/lib/std/math/big/int_test.zig index 2da36f18c2..5a0fda52b2 100644 --- a/lib/std/math/big/int_test.zig +++ b/lib/std/math/big/int_test.zig @@ -17,6 +17,12 @@ const minInt = std.math.minInt; // They will still run on larger than this and should pass, but the multi-limb code-paths // may be untested in some cases. +fn expectNormalized(expected: comptime_int, actual: std.math.big.int.Const) !void { + try testing.expectEqual(expected >= 0, actual.positive); + try testing.expectEqual(std.math.big.int.calcLimbLen(expected), actual.limbs.len); + try testing.expect(actual.orderAgainstScalar(expected).compare(.eq)); +} + test "comptime_int set" { comptime var s = 0xefffffff00000001eeeeeeefaaaaaaab; var a = try Managed.initSet(testing.allocator, s); @@ -85,6 +91,408 @@ test "to target too small error" { try testing.expectError(error.TargetTooSmall, a.toInt(u8)); } +fn setFloat(comptime Float: type) !void { + var res_limbs: [std.math.big.int.calcNonZeroTwosCompLimbCount(11)]Limb = undefined; + var res: Mutable = .{ + .limbs = &res_limbs, + .len = undefined, + .positive = undefined, + }; + + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0x1p10), .nearest_even)); + try expectNormalized(-1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0x1p10), .away)); + try expectNormalized(-1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0x1p10), .trunc)); + try expectNormalized(-1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0x1p10), .floor)); + try expectNormalized(-1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0x1p10), .ceil)); + try expectNormalized(-1 << 10, res.toConst()); + + try testing.expectEqual(.exact, res.setFloat(@as(Float, -2.0), .nearest_even)); + try expectNormalized(-2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -2.0), .away)); + try expectNormalized(-2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -2.0), .trunc)); + try expectNormalized(-2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -2.0), .floor)); + try expectNormalized(-2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -2.0), .ceil)); + try expectNormalized(-2, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -1.5), .nearest_even)); + try expectNormalized(-2, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -1.5), .away)); + try expectNormalized(-2, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -1.5), .trunc)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -1.5), .floor)); + try expectNormalized(-2, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -1.5), .ceil)); + try expectNormalized(-1, res.toConst()); + + try testing.expectEqual(.exact, res.setFloat(@as(Float, -1.0), .nearest_even)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -1.0), .away)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -1.0), .trunc)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -1.0), .floor)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -1.0), .ceil)); + try expectNormalized(-1, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.75), .nearest_even)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.75), .away)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.75), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.75), .floor)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.75), .ceil)); + try expectNormalized(0, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.5), .nearest_even)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.5), .away)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.5), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.5), .floor)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.5), .ceil)); + try expectNormalized(0, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.25), .nearest_even)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.25), .away)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.25), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.25), .floor)); + try expectNormalized(-1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, -0.25), .ceil)); + try expectNormalized(0, res.toConst()); + + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0.0), .nearest_even)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0.0), .away)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0.0), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0.0), .floor)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, -0.0), .ceil)); + try expectNormalized(0, res.toConst()); + + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0.0), .nearest_even)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0.0), .away)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0.0), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0.0), .floor)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0.0), .ceil)); + try expectNormalized(0, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.25), .nearest_even)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.25), .away)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.25), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.25), .floor)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.25), .ceil)); + try expectNormalized(1, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.5), .nearest_even)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.5), .away)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.5), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.5), .floor)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.5), .ceil)); + try expectNormalized(1, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.75), .nearest_even)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.75), .away)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.75), .trunc)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.75), .floor)); + try expectNormalized(0, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 0.75), .ceil)); + try expectNormalized(1, res.toConst()); + + try testing.expectEqual(.exact, res.setFloat(@as(Float, 1.0), .nearest_even)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 1.0), .away)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 1.0), .trunc)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 1.0), .floor)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 1.0), .ceil)); + try expectNormalized(1, res.toConst()); + + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 1.5), .nearest_even)); + try expectNormalized(2, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 1.5), .away)); + try expectNormalized(2, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 1.5), .trunc)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 1.5), .floor)); + try expectNormalized(1, res.toConst()); + try testing.expectEqual(.inexact, res.setFloat(@as(Float, 1.5), .ceil)); + try expectNormalized(2, res.toConst()); + + try testing.expectEqual(.exact, res.setFloat(@as(Float, 2.0), .nearest_even)); + try expectNormalized(2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 2.0), .away)); + try expectNormalized(2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 2.0), .trunc)); + try expectNormalized(2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 2.0), .floor)); + try expectNormalized(2, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 2.0), .ceil)); + try expectNormalized(2, res.toConst()); + + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0x1p10), .nearest_even)); + try expectNormalized(1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0x1p10), .away)); + try expectNormalized(1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0x1p10), .trunc)); + try expectNormalized(1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0x1p10), .floor)); + try expectNormalized(1 << 10, res.toConst()); + try testing.expectEqual(.exact, res.setFloat(@as(Float, 0x1p10), .ceil)); + try expectNormalized(1 << 10, res.toConst()); +} +test setFloat { + if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; + + try setFloat(f16); + try setFloat(f32); + try setFloat(f64); + try setFloat(f80); + try setFloat(f128); + try setFloat(c_longdouble); + try setFloat(comptime_float); +} + +fn toFloat(comptime Float: type) !void { + const Result = struct { Float, std.math.big.int.Exactness }; + const fractional_bits = std.math.floatFractionalBits(Float); + + var int_limbs: [ + std.math.big.int.calcNonZeroTwosCompLimbCount(2 + fractional_bits) + ]Limb = undefined; + var int: Mutable = .{ + .limbs = &int_limbs, + .len = undefined, + .positive = undefined, + }; + + int.set(-(1 << (fractional_bits + 1)) - 1); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .inexact }, + int.toFloat(Float, .nearest_even), + ); + try testing.expectEqual( + Result{ comptime std.math.nextAfter( + Float, + -std.math.ldexp(@as(Float, 1), fractional_bits + 1), + -std.math.inf(Float), + ), .inexact }, + int.toFloat(Float, .away), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .inexact }, + int.toFloat(Float, .trunc), + ); + try testing.expectEqual( + Result{ comptime std.math.nextAfter( + Float, + -std.math.ldexp(@as(Float, 1), fractional_bits + 1), + -std.math.inf(Float), + ), .inexact }, + int.toFloat(Float, .floor), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .inexact }, + int.toFloat(Float, .ceil), + ); + + int.set(-1 << (fractional_bits + 1)); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .nearest_even), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .away), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .trunc), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .floor), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .ceil), + ); + + int.set(-(1 << (fractional_bits + 1)) + 1); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1) + 1.0, .exact }, + int.toFloat(Float, .nearest_even), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1) + 1.0, .exact }, + int.toFloat(Float, .away), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1) + 1.0, .exact }, + int.toFloat(Float, .trunc), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1) + 1.0, .exact }, + int.toFloat(Float, .floor), + ); + try testing.expectEqual( + Result{ comptime -std.math.ldexp(@as(Float, 1), fractional_bits + 1) + 1.0, .exact }, + int.toFloat(Float, .ceil), + ); + + int.set(-1 << 10); + try testing.expectEqual(Result{ -0x1p10, .exact }, int.toFloat(Float, .nearest_even)); + try testing.expectEqual(Result{ -0x1p10, .exact }, int.toFloat(Float, .away)); + try testing.expectEqual(Result{ -0x1p10, .exact }, int.toFloat(Float, .trunc)); + try testing.expectEqual(Result{ -0x1p10, .exact }, int.toFloat(Float, .floor)); + try testing.expectEqual(Result{ -0x1p10, .exact }, int.toFloat(Float, .ceil)); + + int.set(-1); + try testing.expectEqual(Result{ -1.0, .exact }, int.toFloat(Float, .nearest_even)); + try testing.expectEqual(Result{ -1.0, .exact }, int.toFloat(Float, .away)); + try testing.expectEqual(Result{ -1.0, .exact }, int.toFloat(Float, .trunc)); + try testing.expectEqual(Result{ -1.0, .exact }, int.toFloat(Float, .floor)); + try testing.expectEqual(Result{ -1.0, .exact }, int.toFloat(Float, .ceil)); + + int.set(0); + try testing.expectEqual(Result{ 0.0, .exact }, int.toFloat(Float, .nearest_even)); + try testing.expectEqual(Result{ 0.0, .exact }, int.toFloat(Float, .away)); + try testing.expectEqual(Result{ 0.0, .exact }, int.toFloat(Float, .trunc)); + try testing.expectEqual(Result{ 0.0, .exact }, int.toFloat(Float, .floor)); + try testing.expectEqual(Result{ 0.0, .exact }, int.toFloat(Float, .ceil)); + + int.set(1); + try testing.expectEqual(Result{ 1.0, .exact }, int.toFloat(Float, .nearest_even)); + try testing.expectEqual(Result{ 1.0, .exact }, int.toFloat(Float, .away)); + try testing.expectEqual(Result{ 1.0, .exact }, int.toFloat(Float, .trunc)); + try testing.expectEqual(Result{ 1.0, .exact }, int.toFloat(Float, .floor)); + try testing.expectEqual(Result{ 1.0, .exact }, int.toFloat(Float, .ceil)); + + int.set(1 << 10); + try testing.expectEqual(Result{ 0x1p10, .exact }, int.toFloat(Float, .nearest_even)); + try testing.expectEqual(Result{ 0x1p10, .exact }, int.toFloat(Float, .away)); + try testing.expectEqual(Result{ 0x1p10, .exact }, int.toFloat(Float, .trunc)); + try testing.expectEqual(Result{ 0x1p10, .exact }, int.toFloat(Float, .floor)); + try testing.expectEqual(Result{ 0x1p10, .exact }, int.toFloat(Float, .ceil)); + + int.set((1 << (fractional_bits + 1)) - 1); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1) - 1.0, .exact }, + int.toFloat(Float, .nearest_even), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1) - 1.0, .exact }, + int.toFloat(Float, .away), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1) - 1.0, .exact }, + int.toFloat(Float, .trunc), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1) - 1.0, .exact }, + int.toFloat(Float, .floor), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1) - 1.0, .exact }, + int.toFloat(Float, .ceil), + ); + + int.set(1 << (fractional_bits + 1)); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .nearest_even), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .away), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .trunc), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .floor), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .exact }, + int.toFloat(Float, .ceil), + ); + + int.set((1 << (fractional_bits + 1)) + 1); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .inexact }, + int.toFloat(Float, .nearest_even), + ); + try testing.expectEqual( + Result{ comptime std.math.nextAfter( + Float, + std.math.ldexp(@as(Float, 1), fractional_bits + 1), + std.math.inf(Float), + ), .inexact }, + int.toFloat(Float, .away), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .inexact }, + int.toFloat(Float, .trunc), + ); + try testing.expectEqual( + Result{ comptime std.math.ldexp(@as(Float, 1), fractional_bits + 1), .inexact }, + int.toFloat(Float, .floor), + ); + try testing.expectEqual( + Result{ comptime std.math.nextAfter( + Float, + std.math.ldexp(@as(Float, 1), fractional_bits + 1), + std.math.inf(Float), + ), .inexact }, + int.toFloat(Float, .ceil), + ); +} +test toFloat { + if (builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/24191 + try toFloat(f16); + try toFloat(f32); + try toFloat(f64); + try toFloat(f80); + try toFloat(f128); + try toFloat(c_longdouble); +} + test "normalize" { var a = try Managed.init(testing.allocator); defer a.deinit(); diff --git a/lib/std/math/big/rational.zig b/lib/std/math/big/rational.zig deleted file mode 100644 index 08a2c23388..0000000000 --- a/lib/std/math/big/rational.zig +++ /dev/null @@ -1,820 +0,0 @@ -const std = @import("../../std.zig"); -const builtin = @import("builtin"); -const debug = std.debug; -const math = std.math; -const mem = std.mem; -const testing = std.testing; -const Allocator = mem.Allocator; - -const Limb = std.math.big.Limb; -const DoubleLimb = std.math.big.DoubleLimb; -const Int = std.math.big.int.Managed; -const IntConst = std.math.big.int.Const; - -/// An arbitrary-precision rational number. -/// -/// Memory is allocated as needed for operations to ensure full precision is kept. The precision -/// of a Rational is only bounded by memory. -/// -/// Rational's are always normalized. That is, for a Rational r = p/q where p and q are integers, -/// gcd(p, q) = 1 always. -/// -/// TODO rework this to store its own allocator and use a non-managed big int, to avoid double -/// allocator storage. -pub const Rational = struct { - /// Numerator. Determines the sign of the Rational. - p: Int, - - /// Denominator. Sign is ignored. - q: Int, - - /// Create a new Rational. A small amount of memory will be allocated on initialization. - /// This will be 2 * Int.default_capacity. - pub fn init(a: Allocator) !Rational { - var p = try Int.init(a); - errdefer p.deinit(); - return Rational{ - .p = p, - .q = try Int.initSet(a, 1), - }; - } - - /// Frees all memory associated with a Rational. - pub fn deinit(self: *Rational) void { - self.p.deinit(); - self.q.deinit(); - } - - /// Set a Rational from a primitive integer type. - pub fn setInt(self: *Rational, a: anytype) !void { - try self.p.set(a); - try self.q.set(1); - } - - /// Set a Rational from a string of the form `A/B` where A and B are base-10 integers. - pub fn setFloatString(self: *Rational, str: []const u8) !void { - // TODO: Accept a/b fractions and exponent form - if (str.len == 0) { - return error.InvalidFloatString; - } - - const State = enum { - Integer, - Fractional, - }; - - var state = State.Integer; - var point: ?usize = null; - - var start: usize = 0; - if (str[0] == '-') { - start += 1; - } - - for (str, 0..) |c, i| { - switch (state) { - State.Integer => { - switch (c) { - '.' => { - state = State.Fractional; - point = i; - }, - '0'...'9' => { - // okay - }, - else => { - return error.InvalidFloatString; - }, - } - }, - State.Fractional => { - switch (c) { - '0'...'9' => { - // okay - }, - else => { - return error.InvalidFloatString; - }, - } - }, - } - } - - // TODO: batch the multiplies by 10 - if (point) |i| { - try self.p.setString(10, str[0..i]); - - const base = IntConst{ .limbs = &[_]Limb{10}, .positive = true }; - var local_buf: [@sizeOf(Limb) * Int.default_capacity]u8 align(@alignOf(Limb)) = undefined; - var fba = std.heap.FixedBufferAllocator.init(&local_buf); - const base_managed = try base.toManaged(fba.allocator()); - - var j: usize = start; - while (j < str.len - i - 1) : (j += 1) { - try self.p.ensureMulCapacity(self.p.toConst(), base); - try self.p.mul(&self.p, &base_managed); - } - - try self.q.setString(10, str[i + 1 ..]); - try self.p.add(&self.p, &self.q); - - try self.q.set(1); - var k: usize = i + 1; - while (k < str.len) : (k += 1) { - try self.q.mul(&self.q, &base_managed); - } - - try self.reduce(); - } else { - try self.p.setString(10, str[0..]); - try self.q.set(1); - } - } - - /// Set a Rational from a floating-point value. The rational will have enough precision to - /// completely represent the provided float. - pub fn setFloat(self: *Rational, comptime T: type, f: T) !void { - // Translated from golang.go/src/math/big/rat.go. - debug.assert(@typeInfo(T) == .float); - - const UnsignedInt = std.meta.Int(.unsigned, @typeInfo(T).float.bits); - const f_bits = @as(UnsignedInt, @bitCast(f)); - - const exponent_bits = math.floatExponentBits(T); - const exponent_bias = (1 << (exponent_bits - 1)) - 1; - const mantissa_bits = math.floatMantissaBits(T); - - const exponent_mask = (1 << exponent_bits) - 1; - const mantissa_mask = (1 << mantissa_bits) - 1; - - var exponent = @as(i16, @intCast((f_bits >> mantissa_bits) & exponent_mask)); - var mantissa = f_bits & mantissa_mask; - - switch (exponent) { - exponent_mask => { - return error.NonFiniteFloat; - }, - 0 => { - // denormal - exponent -= exponent_bias - 1; - }, - else => { - // normal - mantissa |= 1 << mantissa_bits; - exponent -= exponent_bias; - }, - } - - var shift: i16 = mantissa_bits - exponent; - - // factor out powers of two early from rational - while (mantissa & 1 == 0 and shift > 0) { - mantissa >>= 1; - shift -= 1; - } - - try self.p.set(mantissa); - self.p.setSign(f >= 0); - - try self.q.set(1); - if (shift >= 0) { - try self.q.shiftLeft(&self.q, @as(usize, @intCast(shift))); - } else { - try self.p.shiftLeft(&self.p, @as(usize, @intCast(-shift))); - } - - try self.reduce(); - } - - /// Return a floating-point value that is the closest value to a Rational. - /// - /// The result may not be exact if the Rational is too precise or too large for the - /// target type. - pub fn toFloat(self: Rational, comptime T: type) !T { - // Translated from golang.go/src/math/big/rat.go. - // TODO: Indicate whether the result is not exact. - debug.assert(@typeInfo(T) == .float); - - const fsize = @typeInfo(T).float.bits; - const BitReprType = std.meta.Int(.unsigned, fsize); - - const msize = math.floatMantissaBits(T); - const msize1 = msize + 1; - const msize2 = msize1 + 1; - - const esize = math.floatExponentBits(T); - const ebias = (1 << (esize - 1)) - 1; - const emin = 1 - ebias; - - if (self.p.eqlZero()) { - return 0; - } - - // 1. left-shift a or sub so that a/b is in [1 << msize1, 1 << (msize2 + 1)] - var exp = @as(isize, @intCast(self.p.bitCountTwosComp())) - @as(isize, @intCast(self.q.bitCountTwosComp())); - - var a2 = try self.p.clone(); - defer a2.deinit(); - - var b2 = try self.q.clone(); - defer b2.deinit(); - - const shift = msize2 - exp; - if (shift >= 0) { - try a2.shiftLeft(&a2, @as(usize, @intCast(shift))); - } else { - try b2.shiftLeft(&b2, @as(usize, @intCast(-shift))); - } - - // 2. compute quotient and remainder - var q = try Int.init(self.p.allocator); - defer q.deinit(); - - // unused - var r = try Int.init(self.p.allocator); - defer r.deinit(); - - try Int.divTrunc(&q, &r, &a2, &b2); - - var mantissa = extractLowBits(q, BitReprType); - var have_rem = r.len() > 0; - - // 3. q didn't fit in msize2 bits, redo division b2 << 1 - if (mantissa >> msize2 == 1) { - if (mantissa & 1 == 1) { - have_rem = true; - } - mantissa >>= 1; - exp += 1; - } - if (mantissa >> msize1 != 1) { - // NOTE: This can be hit if the limb size is small (u8/16). - @panic("unexpected bits in result"); - } - - // 4. Rounding - if (emin - msize <= exp and exp <= emin) { - // denormal - const shift1 = @as(math.Log2Int(BitReprType), @intCast(emin - (exp - 1))); - const lost_bits = mantissa & ((@as(BitReprType, @intCast(1)) << shift1) - 1); - have_rem = have_rem or lost_bits != 0; - mantissa >>= shift1; - exp = 2 - ebias; - } - - // round q using round-half-to-even - var exact = !have_rem; - if (mantissa & 1 != 0) { - exact = false; - if (have_rem or (mantissa & 2 != 0)) { - mantissa += 1; - if (mantissa >= 1 << msize2) { - // 11...1 => 100...0 - mantissa >>= 1; - exp += 1; - } - } - } - mantissa >>= 1; - - const f = math.scalbn(@as(T, @floatFromInt(mantissa)), @as(i32, @intCast(exp - msize1))); - if (math.isInf(f)) { - exact = false; - } - - return if (self.p.isPositive()) f else -f; - } - - /// Set a rational from an integer ratio. - pub fn setRatio(self: *Rational, p: anytype, q: anytype) !void { - try self.p.set(p); - try self.q.set(q); - - self.p.setSign(@intFromBool(self.p.isPositive()) ^ @intFromBool(self.q.isPositive()) == 0); - self.q.setSign(true); - - try self.reduce(); - - if (self.q.eqlZero()) { - @panic("cannot set rational with denominator = 0"); - } - } - - /// Set a Rational directly from an Int. - pub fn copyInt(self: *Rational, a: Int) !void { - try self.p.copy(a.toConst()); - try self.q.set(1); - } - - /// Set a Rational directly from a ratio of two Int's. - pub fn copyRatio(self: *Rational, a: Int, b: Int) !void { - try self.p.copy(a.toConst()); - try self.q.copy(b.toConst()); - - self.p.setSign(@intFromBool(self.p.isPositive()) ^ @intFromBool(self.q.isPositive()) == 0); - self.q.setSign(true); - - try self.reduce(); - } - - /// Make a Rational positive. - pub fn abs(r: *Rational) void { - r.p.abs(); - } - - /// Negate the sign of a Rational. - pub fn negate(r: *Rational) void { - r.p.negate(); - } - - /// Efficiently swap a Rational with another. This swaps the limb pointers and a full copy is not - /// performed. The address of the limbs field will not be the same after this function. - pub fn swap(r: *Rational, other: *Rational) void { - r.p.swap(&other.p); - r.q.swap(&other.q); - } - - /// Returns math.Order.lt, math.Order.eq, math.Order.gt if a < b, a == b or - /// a > b respectively. - pub fn order(a: Rational, b: Rational) !math.Order { - return cmpInternal(a, b, false); - } - - /// Returns math.Order.lt, math.Order.eq, math.Order.gt if |a| < |b|, |a| == - /// |b| or |a| > |b| respectively. - pub fn orderAbs(a: Rational, b: Rational) !math.Order { - return cmpInternal(a, b, true); - } - - // p/q > x/y iff p*y > x*q - fn cmpInternal(a: Rational, b: Rational, is_abs: bool) !math.Order { - // TODO: Would a div compare algorithm of sorts be viable and quicker? Can we avoid - // the memory allocations here? - var q = try Int.init(a.p.allocator); - defer q.deinit(); - - var p = try Int.init(b.p.allocator); - defer p.deinit(); - - try q.mul(&a.p, &b.q); - try p.mul(&b.p, &a.q); - - return if (is_abs) q.orderAbs(p) else q.order(p); - } - - /// rma = a + b. - /// - /// rma, a and b may be aliases. However, it is more efficient if rma does not alias a or b. - /// - /// Returns an error if memory could not be allocated. - pub fn add(rma: *Rational, a: Rational, b: Rational) !void { - var r = rma; - var aliased = rma.p.limbs.ptr == a.p.limbs.ptr or rma.p.limbs.ptr == b.p.limbs.ptr; - - var sr: Rational = undefined; - if (aliased) { - sr = try Rational.init(rma.p.allocator); - r = &sr; - aliased = true; - } - defer if (aliased) { - rma.swap(r); - r.deinit(); - }; - - try r.p.mul(&a.p, &b.q); - try r.q.mul(&b.p, &a.q); - try r.p.add(&r.p, &r.q); - - try r.q.mul(&a.q, &b.q); - try r.reduce(); - } - - /// rma = a - b. - /// - /// rma, a and b may be aliases. However, it is more efficient if rma does not alias a or b. - /// - /// Returns an error if memory could not be allocated. - pub fn sub(rma: *Rational, a: Rational, b: Rational) !void { - var r = rma; - var aliased = rma.p.limbs.ptr == a.p.limbs.ptr or rma.p.limbs.ptr == b.p.limbs.ptr; - - var sr: Rational = undefined; - if (aliased) { - sr = try Rational.init(rma.p.allocator); - r = &sr; - aliased = true; - } - defer if (aliased) { - rma.swap(r); - r.deinit(); - }; - - try r.p.mul(&a.p, &b.q); - try r.q.mul(&b.p, &a.q); - try r.p.sub(&r.p, &r.q); - - try r.q.mul(&a.q, &b.q); - try r.reduce(); - } - - /// rma = a * b. - /// - /// rma, a and b may be aliases. However, it is more efficient if rma does not alias a or b. - /// - /// Returns an error if memory could not be allocated. - pub fn mul(r: *Rational, a: Rational, b: Rational) !void { - try r.p.mul(&a.p, &b.p); - try r.q.mul(&a.q, &b.q); - try r.reduce(); - } - - /// rma = a / b. - /// - /// rma, a and b may be aliases. However, it is more efficient if rma does not alias a or b. - /// - /// Returns an error if memory could not be allocated. - pub fn div(r: *Rational, a: Rational, b: Rational) !void { - if (b.p.eqlZero()) { - @panic("division by zero"); - } - - try r.p.mul(&a.p, &b.q); - try r.q.mul(&b.p, &a.q); - try r.reduce(); - } - - /// Invert the numerator and denominator fields of a Rational. p/q => q/p. - pub fn invert(r: *Rational) void { - Int.swap(&r.p, &r.q); - } - - // reduce r/q such that gcd(r, q) = 1 - fn reduce(r: *Rational) !void { - var a = try Int.init(r.p.allocator); - defer a.deinit(); - - const sign = r.p.isPositive(); - r.p.abs(); - try a.gcd(&r.p, &r.q); - r.p.setSign(sign); - - const one = IntConst{ .limbs = &[_]Limb{1}, .positive = true }; - if (a.toConst().order(one) != .eq) { - var unused = try Int.init(r.p.allocator); - defer unused.deinit(); - - // TODO: divexact would be useful here - // TODO: don't copy r.q for div - try Int.divTrunc(&r.p, &unused, &r.p, &a); - try Int.divTrunc(&r.q, &unused, &r.q, &a); - } - } -}; - -fn extractLowBits(a: Int, comptime T: type) T { - debug.assert(@typeInfo(T) == .int); - - const t_bits = @typeInfo(T).int.bits; - const limb_bits = @typeInfo(Limb).int.bits; - if (t_bits <= limb_bits) { - return @as(T, @truncate(a.limbs[0])); - } else { - var r: T = 0; - comptime var i: usize = 0; - - // Remainder is always 0 since if t_bits >= limb_bits -> Limb | T and both - // are powers of two. - inline while (i < t_bits / limb_bits) : (i += 1) { - r |= math.shl(T, a.limbs[i], i * limb_bits); - } - - return r; - } -} - -test extractLowBits { - var a = try Int.initSet(testing.allocator, 0x11112222333344441234567887654321); - defer a.deinit(); - - const a1 = extractLowBits(a, u8); - try testing.expect(a1 == 0x21); - - const a2 = extractLowBits(a, u16); - try testing.expect(a2 == 0x4321); - - const a3 = extractLowBits(a, u32); - try testing.expect(a3 == 0x87654321); - - const a4 = extractLowBits(a, u64); - try testing.expect(a4 == 0x1234567887654321); - - const a5 = extractLowBits(a, u128); - try testing.expect(a5 == 0x11112222333344441234567887654321); -} - -test "set" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - - try a.setInt(5); - try testing.expect((try a.p.toInt(u32)) == 5); - try testing.expect((try a.q.toInt(u32)) == 1); - - try a.setRatio(7, 3); - try testing.expect((try a.p.toInt(u32)) == 7); - try testing.expect((try a.q.toInt(u32)) == 3); - - try a.setRatio(9, 3); - try testing.expect((try a.p.toInt(i32)) == 3); - try testing.expect((try a.q.toInt(i32)) == 1); - - try a.setRatio(-9, 3); - try testing.expect((try a.p.toInt(i32)) == -3); - try testing.expect((try a.q.toInt(i32)) == 1); - - try a.setRatio(9, -3); - try testing.expect((try a.p.toInt(i32)) == -3); - try testing.expect((try a.q.toInt(i32)) == 1); - - try a.setRatio(-9, -3); - try testing.expect((try a.p.toInt(i32)) == 3); - try testing.expect((try a.q.toInt(i32)) == 1); -} - -test "setFloat" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - - try a.setFloat(f64, 2.5); - try testing.expect((try a.p.toInt(i32)) == 5); - try testing.expect((try a.q.toInt(i32)) == 2); - - try a.setFloat(f32, -2.5); - try testing.expect((try a.p.toInt(i32)) == -5); - try testing.expect((try a.q.toInt(i32)) == 2); - - try a.setFloat(f32, 3.141593); - - // = 3.14159297943115234375 - try testing.expect((try a.p.toInt(u32)) == 3294199); - try testing.expect((try a.q.toInt(u32)) == 1048576); - - try a.setFloat(f64, 72.141593120712409172417410926841290461290467124); - - // = 72.1415931207124145885245525278151035308837890625 - try testing.expect((try a.p.toInt(u128)) == 5076513310880537); - try testing.expect((try a.q.toInt(u128)) == 70368744177664); -} - -test "setFloatString" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - - try a.setFloatString("72.14159312071241458852455252781510353"); - - // = 72.1415931207124145885245525278151035308837890625 - try testing.expect((try a.p.toInt(u128)) == 7214159312071241458852455252781510353); - try testing.expect((try a.q.toInt(u128)) == 100000000000000000000000000000000000); -} - -test "toFloat" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - - // = 3.14159297943115234375 - try a.setRatio(3294199, 1048576); - try testing.expect((try a.toFloat(f64)) == 3.14159297943115234375); - - // = 72.1415931207124145885245525278151035308837890625 - try a.setRatio(5076513310880537, 70368744177664); - try testing.expect((try a.toFloat(f64)) == 72.141593120712409172417410926841290461290467124); -} - -test "set/to Float round-trip" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var prng = std.Random.DefaultPrng.init(std.testing.random_seed); - const random = prng.random(); - var i: usize = 0; - while (i < 512) : (i += 1) { - const r = random.float(f64); - try a.setFloat(f64, r); - try testing.expect((try a.toFloat(f64)) == r); - } -} - -test "copy" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - - var b = try Int.initSet(testing.allocator, 5); - defer b.deinit(); - - try a.copyInt(b); - try testing.expect((try a.p.toInt(u32)) == 5); - try testing.expect((try a.q.toInt(u32)) == 1); - - var c = try Int.initSet(testing.allocator, 7); - defer c.deinit(); - var d = try Int.initSet(testing.allocator, 3); - defer d.deinit(); - - try a.copyRatio(c, d); - try testing.expect((try a.p.toInt(u32)) == 7); - try testing.expect((try a.q.toInt(u32)) == 3); - - var e = try Int.initSet(testing.allocator, 9); - defer e.deinit(); - var f = try Int.initSet(testing.allocator, 3); - defer f.deinit(); - - try a.copyRatio(e, f); - try testing.expect((try a.p.toInt(u32)) == 3); - try testing.expect((try a.q.toInt(u32)) == 1); -} - -test "negate" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - - try a.setInt(-50); - try testing.expect((try a.p.toInt(i32)) == -50); - try testing.expect((try a.q.toInt(i32)) == 1); - - a.negate(); - try testing.expect((try a.p.toInt(i32)) == 50); - try testing.expect((try a.q.toInt(i32)) == 1); - - a.negate(); - try testing.expect((try a.p.toInt(i32)) == -50); - try testing.expect((try a.q.toInt(i32)) == 1); -} - -test "abs" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - - try a.setInt(-50); - try testing.expect((try a.p.toInt(i32)) == -50); - try testing.expect((try a.q.toInt(i32)) == 1); - - a.abs(); - try testing.expect((try a.p.toInt(i32)) == 50); - try testing.expect((try a.q.toInt(i32)) == 1); - - a.abs(); - try testing.expect((try a.p.toInt(i32)) == 50); - try testing.expect((try a.q.toInt(i32)) == 1); -} - -test "swap" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - - try a.setRatio(50, 23); - try b.setRatio(17, 3); - - try testing.expect((try a.p.toInt(u32)) == 50); - try testing.expect((try a.q.toInt(u32)) == 23); - - try testing.expect((try b.p.toInt(u32)) == 17); - try testing.expect((try b.q.toInt(u32)) == 3); - - a.swap(&b); - - try testing.expect((try a.p.toInt(u32)) == 17); - try testing.expect((try a.q.toInt(u32)) == 3); - - try testing.expect((try b.p.toInt(u32)) == 50); - try testing.expect((try b.q.toInt(u32)) == 23); -} - -test "order" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - - try a.setRatio(500, 231); - try b.setRatio(18903, 8584); - try testing.expect((try a.order(b)) == .lt); - - try a.setRatio(890, 10); - try b.setRatio(89, 1); - try testing.expect((try a.order(b)) == .eq); -} - -test "order/orderAbs with negative" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - - try a.setRatio(1, 1); - try b.setRatio(-2, 1); - try testing.expect((try a.order(b)) == .gt); - try testing.expect((try a.orderAbs(b)) == .lt); -} - -test "add single-limb" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - - try a.setRatio(500, 231); - try b.setRatio(18903, 8584); - try testing.expect((try a.order(b)) == .lt); - - try a.setRatio(890, 10); - try b.setRatio(89, 1); - try testing.expect((try a.order(b)) == .eq); -} - -test "add" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - var r = try Rational.init(testing.allocator); - defer r.deinit(); - - try a.setRatio(78923, 23341); - try b.setRatio(123097, 12441414); - try a.add(a, b); - - try r.setRatio(984786924199, 290395044174); - try testing.expect((try a.order(r)) == .eq); -} - -test "sub" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - var r = try Rational.init(testing.allocator); - defer r.deinit(); - - try a.setRatio(78923, 23341); - try b.setRatio(123097, 12441414); - try a.sub(a, b); - - try r.setRatio(979040510045, 290395044174); - try testing.expect((try a.order(r)) == .eq); -} - -test "mul" { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - var r = try Rational.init(testing.allocator); - defer r.deinit(); - - try a.setRatio(78923, 23341); - try b.setRatio(123097, 12441414); - try a.mul(a, b); - - try r.setRatio(571481443, 17082061422); - try testing.expect((try a.order(r)) == .eq); -} - -test "div" { - { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var b = try Rational.init(testing.allocator); - defer b.deinit(); - var r = try Rational.init(testing.allocator); - defer r.deinit(); - - try a.setRatio(78923, 23341); - try b.setRatio(123097, 12441414); - try a.div(a, b); - - try r.setRatio(75531824394, 221015929); - try testing.expect((try a.order(r)) == .eq); - } - - { - var a = try Rational.init(testing.allocator); - defer a.deinit(); - var r = try Rational.init(testing.allocator); - defer r.deinit(); - - try a.setRatio(78923, 23341); - a.invert(); - - try r.setRatio(23341, 78923); - try testing.expect((try a.order(r)) == .eq); - - try a.setRatio(-78923, 23341); - a.invert(); - - try r.setRatio(-23341, 78923); - try testing.expect((try a.order(r)) == .eq); - } -} diff --git a/lib/std/math/float.zig b/lib/std/math/float.zig index b7269b3ba9..d545d6390a 100644 --- a/lib/std/math/float.zig +++ b/lib/std/math/float.zig @@ -4,6 +4,119 @@ const assert = std.debug.assert; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; +pub const Sign = enum(u1) { positive, negative }; + +pub fn FloatRepr(comptime Float: type) type { + const fractional_bits = floatFractionalBits(Float); + const exponent_bits = floatExponentBits(Float); + return packed struct { + const Repr = @This(); + + mantissa: StoredMantissa, + exponent: BiasedExponent, + sign: Sign, + + pub const StoredMantissa = @Type(.{ .int = .{ + .signedness = .unsigned, + .bits = floatMantissaBits(Float), + } }); + pub const Mantissa = @Type(.{ .int = .{ + .signedness = .unsigned, + .bits = 1 + fractional_bits, + } }); + pub const Exponent = @Type(.{ .int = .{ + .signedness = .signed, + .bits = exponent_bits, + } }); + pub const BiasedExponent = enum(@Type(.{ .int = .{ + .signedness = .unsigned, + .bits = exponent_bits, + } })) { + denormal = 0, + min_normal = 1, + zero = (1 << (exponent_bits - 1)) - 1, + max_normal = (1 << exponent_bits) - 2, + infinite = (1 << exponent_bits) - 1, + _, + + pub const Int = @typeInfo(BiasedExponent).@"enum".tag_type; + + pub fn unbias(biased: BiasedExponent) Exponent { + switch (biased) { + .denormal => unreachable, + else => return @bitCast(@intFromEnum(biased) -% @intFromEnum(BiasedExponent.zero)), + .infinite => unreachable, + } + } + + pub fn bias(unbiased: Exponent) BiasedExponent { + return @enumFromInt(@intFromEnum(BiasedExponent.zero) +% @as(Int, @bitCast(unbiased))); + } + }; + + pub const Normalized = struct { + fraction: Fraction, + exponent: Normalized.Exponent, + + pub const Fraction = @Type(.{ .int = .{ + .signedness = .unsigned, + .bits = fractional_bits, + } }); + pub const Exponent = @Type(.{ .int = .{ + .signedness = .signed, + .bits = 1 + exponent_bits, + } }); + + /// This currently truncates denormal values, which needs to be fixed before this can be used to + /// produce a rounded value. + pub fn reconstruct(normalized: Normalized, sign: Sign) Float { + if (normalized.exponent > BiasedExponent.max_normal.unbias()) return @bitCast(Repr{ + .mantissa = 0, + .exponent = .infinite, + .sign = sign, + }); + const mantissa = @as(Mantissa, 1 << fractional_bits) | normalized.fraction; + if (normalized.exponent < BiasedExponent.min_normal.unbias()) return @bitCast(Repr{ + .mantissa = @truncate(std.math.shr( + Mantissa, + mantissa, + BiasedExponent.min_normal.unbias() - normalized.exponent, + )), + .exponent = .denormal, + .sign = sign, + }); + return @bitCast(Repr{ + .mantissa = @truncate(mantissa), + .exponent = .bias(@intCast(normalized.exponent)), + .sign = sign, + }); + } + }; + + pub const Classified = union(enum) { normalized: Normalized, infinity, nan, invalid }; + fn classify(repr: Repr) Classified { + return switch (repr.exponent) { + .denormal => { + const mantissa: Mantissa = repr.mantissa; + const shift = @clz(mantissa); + return .{ .normalized = .{ + .fraction = @truncate(mantissa << shift), + .exponent = @as(Normalized.Exponent, comptime BiasedExponent.min_normal.unbias()) - shift, + } }; + }, + else => if (repr.mantissa <= std.math.maxInt(Normalized.Fraction)) .{ .normalized = .{ + .fraction = @intCast(repr.mantissa), + .exponent = repr.exponent.unbias(), + } } else .invalid, + .infinite => switch (repr.mantissa) { + 0 => .infinity, + else => .nan, + }, + }; + } + }; +} + /// Creates a raw "1.0" mantissa for floating point type T. Used to dedupe f80 logic. inline fn mantissaOne(comptime T: type) comptime_int { return if (@typeInfo(T).float.bits == 80) 1 << floatFractionalBits(T) else 0; diff --git a/lib/std/zon/parse.zig b/lib/std/zon/parse.zig index 08447f08f3..8340048805 100644 --- a/lib/std/zon/parse.zig +++ b/lib/std/zon/parse.zig @@ -593,7 +593,7 @@ const Parser = struct { switch (node.get(self.zoir)) { .int_literal => |int| switch (int) { .small => |val| return @floatFromInt(val), - .big => |val| return val.toFloat(T), + .big => |val| return val.toFloat(T, .nearest_even)[0], }, .float_literal => |val| return @floatCast(val), .pos_inf => return std.math.inf(T), 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 01304d78e8..dafb3e5ed7 100644 --- a/src/Air/Legalize.zig +++ b/src/Air/Legalize.zig @@ -1,7 +1,36 @@ pt: Zcu.PerThread, air_instructions: std.MultiArrayList(Air.Inst), air_extra: std.ArrayListUnmanaged(u32), -features: *const Features, +features: if (switch (dev.env) { + .bootstrap => @import("../codegen/c.zig").legalizeFeatures(undefined), + else => null, +}) |bootstrap_features| struct { + fn init(features: *const Features) @This() { + assert(features.eql(bootstrap_features.*)); + return .{}; + } + /// `inline` to propagate comptime-known result. + inline fn has(_: @This(), comptime feature: Feature) bool { + return comptime bootstrap_features.contains(feature); + } + /// `inline` to propagate comptime-known result. + fn hasAny(_: @This(), comptime features: []const Feature) bool { + return comptime !bootstrap_features.intersectWith(.initMany(features)).eql(.initEmpty()); + } +} else struct { + features: *const Features, + /// `inline` to propagate whether `dev.check` returns. + inline fn init(features: *const Features) @This() { + dev.check(.legalize); + return .{ .features = features }; + } + fn has(rt: @This(), comptime feature: Feature) bool { + return rt.features.contains(feature); + } + fn hasAny(rt: @This(), comptime features: []const Feature) bool { + return !rt.features.intersectWith(comptime .initMany(features)).eql(comptime .initEmpty()); + } +}, pub const Feature = enum { scalarize_add, @@ -83,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, @@ -97,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, @@ -196,10 +233,12 @@ 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, - .select => .scalarize_selects, + .select => .scalarize_select, .mul_add => .scalarize_mul_add, }; } @@ -210,13 +249,12 @@ pub const Features = std.enums.EnumSet(Feature); pub const Error = std.mem.Allocator.Error; pub fn legalize(air: *Air, pt: Zcu.PerThread, features: *const Features) Error!void { - dev.check(.legalize); assert(!features.eql(comptime .initEmpty())); // backend asked to run legalize, but no features were enabled var l: Legalize = .{ .pt = pt, .air_instructions = air.instructions.toMultiArrayList(), .air_extra = air.extra, - .features = features, + .features = .init(features), }; defer air.* = l.getTmpAir(); const main_extra = l.extraData(Air.Block, l.air_extra.items[@intFromEnum(Air.ExtraIndex.main_block)]); @@ -278,28 +316,28 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .bit_and, .bit_or, .xor, - => |air_tag| if (l.features.contains(comptime .scalarize(air_tag))) { + => |air_tag| if (l.features.has(comptime .scalarize(air_tag))) { const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op; if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op); }, - .add_safe => if (l.features.contains(.expand_add_safe)) { - assert(!l.features.contains(.scalarize_add_safe)); // it doesn't make sense to do both + .add_safe => if (l.features.has(.expand_add_safe)) { + assert(!l.features.has(.scalarize_add_safe)); // it doesn't make sense to do both continue :inst l.replaceInst(inst, .block, try l.safeArithmeticBlockPayload(inst, .add_with_overflow)); - } else if (l.features.contains(.scalarize_add_safe)) { + } else if (l.features.has(.scalarize_add_safe)) { const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op; if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op); }, - .sub_safe => if (l.features.contains(.expand_sub_safe)) { - assert(!l.features.contains(.scalarize_sub_safe)); // it doesn't make sense to do both + .sub_safe => if (l.features.has(.expand_sub_safe)) { + assert(!l.features.has(.scalarize_sub_safe)); // it doesn't make sense to do both continue :inst l.replaceInst(inst, .block, try l.safeArithmeticBlockPayload(inst, .sub_with_overflow)); - } else if (l.features.contains(.scalarize_sub_safe)) { + } else if (l.features.has(.scalarize_sub_safe)) { const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op; if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op); }, - .mul_safe => if (l.features.contains(.expand_mul_safe)) { - assert(!l.features.contains(.scalarize_mul_safe)); // it doesn't make sense to do both + .mul_safe => if (l.features.has(.expand_mul_safe)) { + assert(!l.features.has(.scalarize_mul_safe)); // it doesn't make sense to do both continue :inst l.replaceInst(inst, .block, try l.safeArithmeticBlockPayload(inst, .mul_with_overflow)); - } else if (l.features.contains(.scalarize_mul_safe)) { + } else if (l.features.has(.scalarize_mul_safe)) { const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op; if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op); }, @@ -308,7 +346,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .sub_with_overflow, .mul_with_overflow, .shl_with_overflow, - => |air_tag| if (l.features.contains(comptime .scalarize(air_tag))) { + => |air_tag| if (l.features.has(comptime .scalarize(air_tag))) { const ty_pl = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_pl; if (ty_pl.ty.toType().fieldType(0, zcu).isVector(zcu)) continue :inst l.replaceInst(inst, .block, try l.scalarizeOverflowBlockPayload(inst)); }, @@ -320,13 +358,13 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .shl, .shl_exact, .shl_sat, - => |air_tag| if (!l.features.intersectWith(comptime .initMany(&.{ + => |air_tag| if (l.features.hasAny(&.{ .unsplat_shift_rhs, .scalarize(air_tag), - })).eql(comptime .initEmpty())) { + })) { const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op; if (l.typeOf(bin_op.rhs).isVector(zcu)) { - if (l.features.contains(.unsplat_shift_rhs)) { + if (l.features.has(.unsplat_shift_rhs)) { if (bin_op.rhs.toInterned()) |rhs_ip_index| switch (ip.indexToKey(rhs_ip_index)) { else => {}, .aggregate => |aggregate| switch (aggregate.storage) { @@ -347,7 +385,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { } } } - if (l.features.contains(comptime .scalarize(air_tag))) continue :inst try l.scalarize(inst, .bin_op); + if (l.features.has(comptime .scalarize(air_tag))) continue :inst try l.scalarize(inst, .bin_op); } }, inline .not, @@ -364,11 +402,11 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .int_from_float, .int_from_float_optimized, .float_from_int, - => |air_tag| if (l.features.contains(comptime .scalarize(air_tag))) { + => |air_tag| if (l.features.has(comptime .scalarize(air_tag))) { 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); }, - .bitcast => if (l.features.contains(.scalarize_bitcast)) { + .bitcast => if (l.features.has(.scalarize_bitcast)) { const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op; const to_ty = ty_op.ty.toType(); @@ -404,10 +442,24 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { }; if (!from_ty_legal) continue :inst l.replaceInst(inst, .block, try l.scalarizeBitcastOperandBlockPayload(inst)); }, - .intcast_safe => if (l.features.contains(.expand_intcast_safe)) { - assert(!l.features.contains(.scalarize_intcast_safe)); // it doesn't make sense to do both + .intcast_safe => if (l.features.has(.expand_intcast_safe)) { + assert(!l.features.has(.scalarize_intcast_safe)); // it doesn't make sense to do both continue :inst l.replaceInst(inst, .block, try l.safeIntcastBlockPayload(inst)); - } else if (l.features.contains(.scalarize_intcast_safe)) { + } else if (l.features.has(.scalarize_intcast_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_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); }, @@ -442,7 +494,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .trunc_float, .neg, .neg_optimized, - => |air_tag| if (l.features.contains(comptime .scalarize(air_tag))) { + => |air_tag| if (l.features.has(comptime .scalarize(air_tag))) { const un_op = l.air_instructions.items(.data)[@intFromEnum(inst)].un_op; if (l.typeOf(un_op).isVector(zcu)) continue :inst try l.scalarize(inst, .un_op); }, @@ -459,7 +511,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .cmp_neq, .cmp_neq_optimized, => {}, - inline .cmp_vector, .cmp_vector_optimized => |air_tag| if (l.features.contains(comptime .scalarize(air_tag))) { + inline .cmp_vector, .cmp_vector_optimized => |air_tag| if (l.features.has(comptime .scalarize(air_tag))) { const ty_pl = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_pl; if (ty_pl.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .cmp_vector); }, @@ -513,13 +565,13 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .bool_and, .bool_or, => {}, - .load => if (l.features.contains(.expand_packed_load)) { + .load => if (l.features.has(.expand_packed_load)) { const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op; const ptr_info = l.typeOf(ty_op.operand).ptrInfo(zcu); if (ptr_info.packed_offset.host_size > 0 and ptr_info.flags.vector_index == .none) continue :inst l.replaceInst(inst, .block, try l.packedLoadBlockPayload(inst)); }, .ret, .ret_safe, .ret_load => {}, - .store, .store_safe => if (l.features.contains(.expand_packed_store)) { + .store, .store_safe => if (l.features.has(.expand_packed_store)) { const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op; const ptr_info = l.typeOf(bin_op.lhs).ptrInfo(zcu); if (ptr_info.packed_offset.host_size > 0 and ptr_info.flags.vector_index == .none) continue :inst l.replaceInst(inst, .block, try l.packedStoreBlockPayload(inst)); @@ -542,7 +594,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .struct_field_ptr_index_2, .struct_field_ptr_index_3, => {}, - .struct_field_val => if (l.features.contains(.expand_packed_struct_field_val)) { + .struct_field_val => if (l.features.has(.expand_packed_struct_field_val)) { const ty_pl = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_pl; const extra = l.extraData(Air.StructField, ty_pl.payload).data; switch (l.typeOf(extra.struct_operand).containerLayout(zcu)) { @@ -564,7 +616,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .ptr_elem_ptr, .array_to_slice, => {}, - .reduce, .reduce_optimized => if (l.features.contains(.reduce_one_elem_to_bitcast)) { + .reduce, .reduce_optimized => if (l.features.has(.reduce_one_elem_to_bitcast)) { const reduce = l.air_instructions.items(.data)[@intFromEnum(inst)].reduce; const vector_ty = l.typeOf(reduce.operand); switch (vector_ty.vectorLen(zcu)) { @@ -577,9 +629,9 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { } }, .splat => {}, - .shuffle_one => if (l.features.contains(.scalarize_shuffle_one)) continue :inst try l.scalarize(inst, .shuffle_one), - .shuffle_two => if (l.features.contains(.scalarize_shuffle_two)) continue :inst try l.scalarize(inst, .shuffle_two), - .select => if (l.features.contains(.scalarize_select)) continue :inst try l.scalarize(inst, .select), + .shuffle_one => if (l.features.has(.scalarize_shuffle_one)) continue :inst try l.scalarize(inst, .shuffle_one), + .shuffle_two => if (l.features.has(.scalarize_shuffle_two)) continue :inst try l.scalarize(inst, .shuffle_two), + .select => if (l.features.has(.scalarize_select)) continue :inst try l.scalarize(inst, .select), .memset, .memset_safe, .memcpy, @@ -597,7 +649,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { .error_name, .error_set_has_value, => {}, - .aggregate_init => if (l.features.contains(.expand_packed_aggregate_init)) { + .aggregate_init => if (l.features.has(.expand_packed_aggregate_init)) { const ty_pl = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_pl; const agg_ty = ty_pl.ty.toType(); switch (agg_ty.zigTypeTag(zcu)) { @@ -609,7 +661,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { } }, .union_init, .prefetch => {}, - .mul_add => if (l.features.contains(.scalarize_mul_add)) { + .mul_add => if (l.features.has(.scalarize_mul_add)) { const pl_op = l.air_instructions.items(.data)[@intFromEnum(inst)].pl_op; if (l.typeOf(pl_op.operand).isVector(zcu)) continue :inst try l.scalarize(inst, .pl_op_bin); }, @@ -636,6 +688,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void { } const ScalarizeForm = enum { un_op, ty_op, bin_op, pl_op_bin, bitcast, cmp_vector, shuffle_one, shuffle_two, select }; +/// inline to propagate comptime-known `replaceInst` result. inline fn scalarize(l: *Legalize, orig_inst: Air.Inst.Index, comptime form: ScalarizeForm) Error!Air.Inst.Tag { return l.replaceInst(orig_inst, .block, try l.scalarizeBlockPayload(orig_inst, form)); } @@ -1972,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; @@ -2349,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, @@ -2691,7 +2889,7 @@ fn addBlockBody(l: *Legalize, body: []const Air.Inst.Index) Error!u32 { } /// Returns `tag` to remind the caller to `continue :inst` the result. -/// This is inline to propagate the comptime-known `tag`. +/// `inline` to propagate the comptime-known `tag` result. inline fn replaceInst(l: *Legalize, inst: Air.Inst.Index, comptime tag: Air.Inst.Tag, data: Air.Inst.Data) Air.Inst.Tag { const orig_ty = if (std.debug.runtime_safety) l.typeOfIndex(inst) else {}; l.air_instructions.set(@intFromEnum(inst), .{ .tag = tag, .data = data }); @@ -2706,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 7b378e4381..b917e9a31f 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -8934,9 +8934,7 @@ fn zirEnumFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError try sema.requireRuntimeBlock(block, src, operand_src); if (block.wantSafety()) { - if (zcu.backendSupportsFeature(.panic_fn)) { - _ = try sema.preparePanicId(src, .invalid_enum_value); - } + try sema.preparePanicId(src, .invalid_enum_value); return block.addTyOp(.intcast_safe, dest_ty, operand); } return block.addTyOp(.intcast, dest_ty, operand); @@ -10340,9 +10338,7 @@ fn intCast( try sema.requireRuntimeBlock(block, src, operand_src); if (block.wantSafety()) { - if (zcu.backendSupportsFeature(.panic_fn)) { - _ = try sema.preparePanicId(src, .integer_out_of_bounds); - } + try sema.preparePanicId(src, .integer_out_of_bounds); return block.addTyOp(.intcast_safe, dest_ty, operand); } return block.addTyOp(.intcast, dest_ty, operand); @@ -16395,9 +16391,7 @@ fn analyzeArithmetic( } if (block.wantSafety() and want_safety and scalar_tag == .int) { - if (air_tag != air_tag_safe and zcu.backendSupportsFeature(.panic_fn)) { - _ = try sema.preparePanicId(src, .integer_overflow); - } + if (air_tag != air_tag_safe) try sema.preparePanicId(src, .integer_overflow); return block.addBinOp(air_tag_safe, casted_lhs, casted_rhs); } return block.addBinOp(air_tag, casted_lhs, casted_rhs); @@ -22178,44 +22172,32 @@ 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); + 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 { @@ -26871,7 +26853,15 @@ fn explainWhyTypeIsNotPacked( /// Backends depend on panic decls being available when lowering safety-checked /// instructions. This function ensures the panic function will be available to /// be called during that time. -fn preparePanicId(sema: *Sema, src: LazySrcLoc, panic_id: Zcu.SimplePanicId) !InternPool.Index { +fn preparePanicId(sema: *Sema, src: LazySrcLoc, panic_id: Zcu.SimplePanicId) !void { + // If the backend doesn't support `.panic_fn`, it doesn't want us to lower the panic handlers. + // The backend will transform panics into traps instead. + if (sema.pt.zcu.backendSupportsFeature(.panic_fn)) { + _ = try sema.getPanicIdFunc(src, panic_id); + } +} + +fn getPanicIdFunc(sema: *Sema, src: LazySrcLoc, panic_id: Zcu.SimplePanicId) !InternPool.Index { const zcu = sema.pt.zcu; try sema.ensureMemoizedStateResolved(src, .panic); const panic_func = zcu.builtin_decl_values.get(panic_id.toBuiltin()); @@ -27120,7 +27110,7 @@ fn safetyPanic(sema: *Sema, block: *Block, src: LazySrcLoc, panic_id: Zcu.Simple if (!sema.pt.zcu.backendSupportsFeature(.panic_fn)) { _ = try block.addNoOp(.trap); } else { - const panic_fn = try sema.preparePanicId(src, panic_id); + const panic_fn = try sema.getPanicIdFunc(src, panic_id); try sema.callBuiltin(block, src, Air.internedToRef(panic_fn), .auto, &.{}, .@"safety check"); } } @@ -32843,24 +32833,21 @@ fn cmpNumeric( } } if (lhs_is_float) { - if (lhs_val.floatHasFraction(zcu)) { - switch (op) { + const float = lhs_val.toFloat(f128, zcu); + var big_int: std.math.big.int.Mutable = .{ + .limbs = try sema.arena.alloc(std.math.big.Limb, std.math.big.int.calcLimbLen(float)), + .len = undefined, + .positive = undefined, + }; + switch (big_int.setFloat(float, .away)) { + .inexact => switch (op) { .eq => return .bool_false, .neq => return .bool_true, else => {}, - } + }, + .exact => {}, } - - var bigint = try float128IntPartToBigInt(sema.gpa, lhs_val.toFloat(f128, zcu)); - defer bigint.deinit(); - if (lhs_val.floatHasFraction(zcu)) { - if (lhs_is_signed) { - try bigint.addScalar(&bigint, -1); - } else { - try bigint.addScalar(&bigint, 1); - } - } - lhs_bits = bigint.toConst().bitCountTwosComp(); + lhs_bits = big_int.toConst().bitCountTwosComp(); } else { lhs_bits = lhs_val.intBitCountTwosComp(zcu); } @@ -32890,24 +32877,21 @@ fn cmpNumeric( } } if (rhs_is_float) { - if (rhs_val.floatHasFraction(zcu)) { - switch (op) { + const float = rhs_val.toFloat(f128, zcu); + var big_int: std.math.big.int.Mutable = .{ + .limbs = try sema.arena.alloc(std.math.big.Limb, std.math.big.int.calcLimbLen(float)), + .len = undefined, + .positive = undefined, + }; + switch (big_int.setFloat(float, .away)) { + .inexact => switch (op) { .eq => return .bool_false, .neq => return .bool_true, else => {}, - } + }, + .exact => {}, } - - var bigint = try float128IntPartToBigInt(sema.gpa, rhs_val.toFloat(f128, zcu)); - defer bigint.deinit(); - if (rhs_val.floatHasFraction(zcu)) { - if (rhs_is_signed) { - try bigint.addScalar(&bigint, -1); - } else { - try bigint.addScalar(&bigint, 1); - } - } - rhs_bits = bigint.toConst().bitCountTwosComp(); + rhs_bits = big_int.toConst().bitCountTwosComp(); } else { rhs_bits = rhs_val.intBitCountTwosComp(zcu); } @@ -36955,31 +36939,6 @@ fn intFromFloat( return sema.intFromFloatScalar(block, src, val, int_ty, mode); } -// float is expected to be finite and non-NaN -fn float128IntPartToBigInt( - arena: Allocator, - float: f128, -) !std.math.big.int.Managed { - const is_negative = std.math.signbit(float); - const floored = @floor(@abs(float)); - - var rational = try std.math.big.Rational.init(arena); - defer rational.q.deinit(); - rational.setFloat(f128, floored) catch |err| switch (err) { - error.NonFiniteFloat => unreachable, - error.OutOfMemory => return error.OutOfMemory, - }; - - // The float is reduced in rational.setFloat, so we assert that denominator is equal to one - const big_one = std.math.big.int.Const{ .limbs = &.{1}, .positive = true }; - assert(rational.q.toConst().eqlAbs(big_one)); - - if (is_negative) { - rational.negate(); - } - return rational.p; -} - fn intFromFloatScalar( sema: *Sema, block: *Block, @@ -36993,13 +36952,6 @@ fn intFromFloatScalar( if (val.isUndef(zcu)) return sema.failWithUseOfUndef(block, src); - if (mode == .exact and val.floatHasFraction(zcu)) return sema.fail( - block, - src, - "fractional component prevents float value '{}' from coercion to type '{}'", - .{ val.fmtValueSema(pt, sema), int_ty.fmt(pt) }, - ); - const float = val.toFloat(f128, zcu); if (std.math.isNan(float)) { return sema.fail(block, src, "float value NaN cannot be stored in integer type '{}'", .{ @@ -37012,12 +36964,28 @@ fn intFromFloatScalar( }); } - var big_int = try float128IntPartToBigInt(sema.arena, float); - defer big_int.deinit(); - + var big_int: std.math.big.int.Mutable = .{ + .limbs = try sema.arena.alloc(std.math.big.Limb, std.math.big.int.calcLimbLen(float)), + .len = undefined, + .positive = undefined, + }; + switch (big_int.setFloat(float, .trunc)) { + .inexact => switch (mode) { + .exact => return sema.fail( + block, + src, + "fractional component prevents float value '{}' from coercion to type '{}'", + .{ val.fmtValueSema(pt, sema), int_ty.fmt(pt) }, + ), + .truncate => {}, + }, + .exact => {}, + } const cti_result = try pt.intValue_big(.comptime_int, big_int.toConst()); + if (int_ty.toIntern() == .comptime_int_type) return cti_result; - if (!(try sema.intFitsInType(cti_result, int_ty, null))) { + const int_info = int_ty.intInfo(zcu); + if (!big_int.toConst().fitsInTwosComp(int_info.signedness, int_info.bits)) { return sema.fail(block, src, "float value '{}' cannot be stored in integer type '{}'", .{ val.fmtValueSema(pt, sema), int_ty.fmt(pt), }); diff --git a/src/Sema/LowerZon.zig b/src/Sema/LowerZon.zig index 8dfb710ac0..9bd876082f 100644 --- a/src/Sema/LowerZon.zig +++ b/src/Sema/LowerZon.zig @@ -509,30 +509,23 @@ fn lowerInt( }, }, .float_literal => |val| { - // Check for fractional components - if (@rem(val, 1) != 0) { - return self.fail( + var big_int: std.math.big.int.Mutable = .{ + .limbs = try self.sema.arena.alloc(std.math.big.Limb, std.math.big.int.calcLimbLen(val)), + .len = undefined, + .positive = undefined, + }; + switch (big_int.setFloat(val, .trunc)) { + .inexact => return self.fail( node, "fractional component prevents float value '{}' from coercion to type '{}'", .{ val, res_ty.fmt(self.sema.pt) }, - ); + ), + .exact => {}, } - // Create a rational representation of the float - var rational = try std.math.big.Rational.init(self.sema.arena); - rational.setFloat(f128, val) catch |err| switch (err) { - error.NonFiniteFloat => unreachable, - error.OutOfMemory => return error.OutOfMemory, - }; - - // The float is reduced in rational.setFloat, so we assert that denominator is equal to - // one - const big_one = std.math.big.int.Const{ .limbs = &.{1}, .positive = true }; - assert(rational.q.toConst().eqlAbs(big_one)); - // Check that the result is in range of the result type const int_info = res_ty.intInfo(self.sema.pt.zcu); - if (!rational.p.fitsInTwosComp(int_info.signedness, int_info.bits)) { + if (!big_int.toConst().fitsInTwosComp(int_info.signedness, int_info.bits)) { return self.fail( node, "type '{}' cannot represent integer value '{}'", @@ -543,7 +536,7 @@ fn lowerInt( return self.sema.pt.intern(.{ .int = .{ .ty = res_ty.toIntern(), - .storage = .{ .big_int = rational.p.toConst() }, + .storage = .{ .big_int = big_int.toConst() }, }, }); }, @@ -584,7 +577,7 @@ fn lowerFloat( const value = switch (node.get(self.file.zoir.?)) { .int_literal => |int| switch (int) { .small => |val| try self.sema.pt.floatValue(res_ty, @as(f128, @floatFromInt(val))), - .big => |val| try self.sema.pt.floatValue(res_ty, val.toFloat(f128)), + .big => |val| try self.sema.pt.floatValue(res_ty, val.toFloat(f128, .nearest_even)[0]), }, .float_literal => |val| try self.sema.pt.floatValue(res_ty, val), .char_literal => |val| try self.sema.pt.floatValue(res_ty, @as(f128, @floatFromInt(val))), diff --git a/src/Value.zig b/src/Value.zig index 2b7868923b..96206b6c26 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -898,7 +898,7 @@ pub fn readFromPackedMemory( pub fn toFloat(val: Value, comptime T: type, zcu: *const Zcu) T { return switch (zcu.intern_pool.indexToKey(val.toIntern())) { .int => |int| switch (int.storage) { - .big_int => |big_int| big_int.toFloat(T), + .big_int => |big_int| big_int.toFloat(T, .nearest_even)[0], inline .u64, .i64 => |x| { if (T == f80) { @panic("TODO we can't lower this properly on non-x86 llvm backend yet"); @@ -997,16 +997,6 @@ pub fn floatCast(val: Value, dest_ty: Type, pt: Zcu.PerThread) !Value { } })); } -/// Asserts the value is a float -pub fn floatHasFraction(self: Value, zcu: *const Zcu) bool { - return switch (zcu.intern_pool.indexToKey(self.toIntern())) { - .float => |float| switch (float.storage) { - inline else => |x| @rem(x, 1) != 0, - }, - else => unreachable, - }; -} - pub fn orderAgainstZero(lhs: Value, zcu: *Zcu) std.math.Order { return orderAgainstZeroInner(lhs, .normal, zcu, {}) catch unreachable; } @@ -1557,17 +1547,13 @@ pub fn floatFromIntAdvanced( } pub fn floatFromIntScalar(val: Value, float_ty: Type, pt: Zcu.PerThread, comptime strat: ResolveStrat) !Value { - const zcu = pt.zcu; - return switch (zcu.intern_pool.indexToKey(val.toIntern())) { + return switch (pt.zcu.intern_pool.indexToKey(val.toIntern())) { .undef => try pt.undefValue(float_ty), .int => |int| switch (int.storage) { - .big_int => |big_int| { - const float = big_int.toFloat(f128); - return pt.floatValue(float_ty, float); - }, + .big_int => |big_int| pt.floatValue(float_ty, big_int.toFloat(f128, .nearest_even)[0]), inline .u64, .i64 => |x| floatFromIntInner(x, float_ty, pt), - .lazy_align => |ty| return floatFromIntInner((try Type.fromInterned(ty).abiAlignmentInner(strat.toLazy(), pt.zcu, pt.tid)).scalar.toByteUnits() orelse 0, float_ty, pt), - .lazy_size => |ty| return floatFromIntInner((try Type.fromInterned(ty).abiSizeInner(strat.toLazy(), pt.zcu, pt.tid)).scalar, float_ty, pt), + .lazy_align => |ty| floatFromIntInner((try Type.fromInterned(ty).abiAlignmentInner(strat.toLazy(), pt.zcu, pt.tid)).scalar.toByteUnits() orelse 0, float_ty, pt), + .lazy_size => |ty| floatFromIntInner((try Type.fromInterned(ty).abiSizeInner(strat.toLazy(), pt.zcu, pt.tid)).scalar, float_ty, pt), }, else => unreachable, }; 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 f4952d4a58..110c5a3127 100644 --- a/src/codegen/c.zig +++ b/src/codegen/c.zig @@ -23,12 +23,21 @@ const BigIntLimb = std.math.big.Limb; const BigInt = std.math.big.int; pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features { - return if (dev.env.supports(.legalize)) comptime &.initMany(&.{ - .expand_intcast_safe, - .expand_add_safe, - .expand_sub_safe, - .expand_mul_safe, - }) else null; // we don't currently ask zig1 to use safe optimization modes + return comptime switch (dev.env.supports(.legalize)) { + 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, + + .expand_packed_load = true, + .expand_packed_store = true, + .expand_packed_struct_field_val = true, + }), + }; } /// For most backends, MIR is basically a sequence of machine code instructions, perhaps with some @@ -3571,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/stage1/zig.h b/stage1/zig.h index 229d6a7973..188800cde4 100644 --- a/stage1/zig.h +++ b/stage1/zig.h @@ -272,6 +272,15 @@ #define zig_linksection_fn zig_linksection #endif +#if zig_has_attribute(visibility) +#define zig_visibility(name) __attribute__((visibility(#name))) +#else +#define zig_visibility(name) zig_visibility_##name +#define zig_visibility_default +#define zig_visibility_hidden zig_visibility_hidden_unavailable +#define zig_visibility_protected zig_visibility_protected_unavailable +#endif + #if zig_has_builtin(unreachable) || defined(zig_gcc) || defined(zig_tinyc) #define zig_unreachable() __builtin_unreachable() #elif defined(zig_msvc) diff --git a/stage1/zig1.wasm b/stage1/zig1.wasm index bb14b1b213..8457058d61 100644 Binary files a/stage1/zig1.wasm and b/stage1/zig1.wasm differ diff --git a/test/behavior/cast.zig b/test/behavior/cast.zig index e21cf7ea12..d96f658ef8 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