mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
348 lines
12 KiB
Zig
348 lines
12 KiB
Zig
// The rounding logic is inspired by LLVM's APFloat and Go's atofHex
|
|
// implementation.
|
|
|
|
const std = @import("std");
|
|
const ascii = std.ascii;
|
|
const fmt = std.fmt;
|
|
const math = std.math;
|
|
const testing = std.testing;
|
|
|
|
const assert = std.debug.assert;
|
|
|
|
pub fn parseHexFloat(comptime T: type, s: []const u8) !T {
|
|
assert(@typeInfo(T) == .Float);
|
|
|
|
const TBits = std.meta.Int(.unsigned, @typeInfo(T).Float.bits);
|
|
|
|
const mantissa_bits = math.floatMantissaBits(T);
|
|
const exponent_bits = math.floatExponentBits(T);
|
|
const exponent_min = math.floatExponentMin(T);
|
|
const exponent_max = math.floatExponentMax(T);
|
|
|
|
const exponent_bias = exponent_max;
|
|
const sign_shift = mantissa_bits + exponent_bits;
|
|
|
|
if (s.len == 0)
|
|
return error.InvalidCharacter;
|
|
|
|
if (ascii.eqlIgnoreCase(s, "nan")) {
|
|
return math.nan(T);
|
|
} else if (ascii.eqlIgnoreCase(s, "inf") or ascii.eqlIgnoreCase(s, "+inf")) {
|
|
return math.inf(T);
|
|
} else if (ascii.eqlIgnoreCase(s, "-inf")) {
|
|
return -math.inf(T);
|
|
}
|
|
|
|
var negative: bool = false;
|
|
var exp_negative: bool = false;
|
|
|
|
var mantissa: u128 = 0;
|
|
var exponent: i16 = 0;
|
|
var frac_scale: i16 = 0;
|
|
|
|
const State = enum {
|
|
MaybeSign,
|
|
Prefix,
|
|
LeadingIntegerDigit,
|
|
IntegerDigit,
|
|
MaybeDot,
|
|
LeadingFractionDigit,
|
|
FractionDigit,
|
|
ExpPrefix,
|
|
MaybeExpSign,
|
|
ExpDigit,
|
|
};
|
|
|
|
var state = State.MaybeSign;
|
|
|
|
var i: usize = 0;
|
|
while (i < s.len) {
|
|
const c = s[i];
|
|
|
|
switch (state) {
|
|
.MaybeSign => {
|
|
state = .Prefix;
|
|
|
|
if (c == '+') {
|
|
i += 1;
|
|
} else if (c == '-') {
|
|
negative = true;
|
|
i += 1;
|
|
}
|
|
},
|
|
.Prefix => {
|
|
state = .LeadingIntegerDigit;
|
|
|
|
// Match both 0x and 0X.
|
|
if (i + 2 > s.len or s[i] != '0' or s[i + 1] | 32 != 'x')
|
|
return error.InvalidCharacter;
|
|
i += 2;
|
|
},
|
|
.LeadingIntegerDigit => {
|
|
if (c == '0') {
|
|
// Skip leading zeros.
|
|
i += 1;
|
|
} else if (c == '_') {
|
|
return error.InvalidCharacter;
|
|
} else {
|
|
state = .IntegerDigit;
|
|
}
|
|
},
|
|
.IntegerDigit => {
|
|
if (ascii.isXDigit(c)) {
|
|
if (mantissa >= math.maxInt(u128) / 16)
|
|
return error.Overflow;
|
|
mantissa *%= 16;
|
|
mantissa += try fmt.charToDigit(c, 16);
|
|
i += 1;
|
|
} else if (c == '_') {
|
|
i += 1;
|
|
} else {
|
|
state = .MaybeDot;
|
|
}
|
|
},
|
|
.MaybeDot => {
|
|
if (c == '.') {
|
|
state = .LeadingFractionDigit;
|
|
i += 1;
|
|
} else state = .ExpPrefix;
|
|
},
|
|
.LeadingFractionDigit => {
|
|
if (c == '_') {
|
|
return error.InvalidCharacter;
|
|
} else state = .FractionDigit;
|
|
},
|
|
.FractionDigit => {
|
|
if (ascii.isXDigit(c)) {
|
|
if (mantissa < math.maxInt(u128) / 16) {
|
|
mantissa *%= 16;
|
|
mantissa +%= try fmt.charToDigit(c, 16);
|
|
frac_scale += 1;
|
|
} else if (c != '0') {
|
|
return error.Overflow;
|
|
}
|
|
i += 1;
|
|
} else if (c == '_') {
|
|
i += 1;
|
|
} else {
|
|
state = .ExpPrefix;
|
|
}
|
|
},
|
|
.ExpPrefix => {
|
|
state = .MaybeExpSign;
|
|
// Match both p and P.
|
|
if (c | 32 != 'p')
|
|
return error.InvalidCharacter;
|
|
i += 1;
|
|
},
|
|
.MaybeExpSign => {
|
|
state = .ExpDigit;
|
|
|
|
if (c == '+') {
|
|
i += 1;
|
|
} else if (c == '-') {
|
|
exp_negative = true;
|
|
i += 1;
|
|
}
|
|
},
|
|
.ExpDigit => {
|
|
if (ascii.isXDigit(c)) {
|
|
if (exponent >= math.maxInt(i16) / 10)
|
|
return error.Overflow;
|
|
exponent *%= 10;
|
|
exponent +%= try fmt.charToDigit(c, 10);
|
|
i += 1;
|
|
} else if (c == '_') {
|
|
i += 1;
|
|
} else {
|
|
return error.InvalidCharacter;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
if (exp_negative)
|
|
exponent *= -1;
|
|
|
|
// Bring the decimal part to the left side of the decimal dot.
|
|
exponent -= frac_scale * 4;
|
|
|
|
if (mantissa == 0) {
|
|
// Signed zero.
|
|
return if (negative) -0.0 else 0.0;
|
|
}
|
|
|
|
// Divide by 2^mantissa_bits to right-align the mantissa in the fractional
|
|
// part.
|
|
exponent += mantissa_bits;
|
|
|
|
// Keep around two extra bits to correctly round any value that doesn't fit
|
|
// the available mantissa bits. The result LSB serves as Guard bit, the
|
|
// following one is the Round bit and the last one is the Sticky bit,
|
|
// computed by OR-ing all the dropped bits.
|
|
|
|
// Normalize by aligning the implicit one bit.
|
|
while (mantissa >> (mantissa_bits + 2) == 0) {
|
|
mantissa <<= 1;
|
|
exponent -= 1;
|
|
}
|
|
|
|
// Normalize again by dropping the excess precision.
|
|
// Note that the discarded bits are folded into the Sticky bit.
|
|
while (mantissa >> (mantissa_bits + 2 + 1) != 0) {
|
|
mantissa = mantissa >> 1 | (mantissa & 1);
|
|
exponent += 1;
|
|
}
|
|
|
|
// Very small numbers can be possibly represented as denormals, reduce the
|
|
// exponent as much as possible.
|
|
while (mantissa != 0 and exponent < exponent_min - 2) {
|
|
mantissa = mantissa >> 1 | (mantissa & 1);
|
|
exponent += 1;
|
|
}
|
|
|
|
// Whenever the guard bit is one (G=1) and:
|
|
// - we've truncated more than 0.5ULP (R=S=1)
|
|
// - we've truncated exactly 0.5ULP (R=1 S=0)
|
|
// Were are going to increase the mantissa (round up)
|
|
const guard_bit_and_half_or_more = (mantissa & 0b110) == 0b110;
|
|
mantissa >>= 2;
|
|
exponent += 2;
|
|
|
|
if (guard_bit_and_half_or_more) {
|
|
mantissa += 1;
|
|
}
|
|
|
|
if (mantissa == (1 << (mantissa_bits + 1))) {
|
|
// Renormalize, if the exponent overflows we'll catch that below.
|
|
mantissa >>= 1;
|
|
exponent += 1;
|
|
}
|
|
|
|
if (mantissa >> mantissa_bits == 0) {
|
|
// This is a denormal number, the biased exponent is zero.
|
|
exponent = -exponent_bias;
|
|
}
|
|
|
|
if (exponent > exponent_max) {
|
|
// Overflow, return +inf.
|
|
return math.inf(T);
|
|
}
|
|
|
|
// Remove the implicit bit.
|
|
mantissa &= @as(u128, (1 << mantissa_bits) - 1);
|
|
|
|
const raw: TBits =
|
|
(if (negative) @as(TBits, 1) << sign_shift else 0) |
|
|
@as(TBits, @bitCast(u16, exponent + exponent_bias)) << mantissa_bits |
|
|
@truncate(TBits, mantissa);
|
|
|
|
return @bitCast(T, raw);
|
|
}
|
|
|
|
test "special" {
|
|
try testing.expect(math.isNan(try parseHexFloat(f32, "nAn")));
|
|
try testing.expect(math.isPositiveInf(try parseHexFloat(f32, "iNf")));
|
|
try testing.expect(math.isPositiveInf(try parseHexFloat(f32, "+Inf")));
|
|
try testing.expect(math.isNegativeInf(try parseHexFloat(f32, "-iNf")));
|
|
}
|
|
test "zero" {
|
|
try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0"));
|
|
try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "-0x0"));
|
|
try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0p42"));
|
|
try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "-0x0.00000p42"));
|
|
try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0.00000p666"));
|
|
}
|
|
|
|
test "f16" {
|
|
const Case = struct { s: []const u8, v: f16 };
|
|
const cases: []const Case = &[_]Case{
|
|
.{ .s = "0x1p0", .v = 1.0 },
|
|
.{ .s = "-0x1p-1", .v = -0.5 },
|
|
.{ .s = "0x10p+10", .v = 16384.0 },
|
|
.{ .s = "0x10p-10", .v = 0.015625 },
|
|
// Max normalized value.
|
|
.{ .s = "0x1.ffcp+15", .v = math.floatMax(f16) },
|
|
.{ .s = "-0x1.ffcp+15", .v = -math.floatMax(f16) },
|
|
// Min normalized value.
|
|
.{ .s = "0x1p-14", .v = math.floatMin(f16) },
|
|
.{ .s = "-0x1p-14", .v = -math.floatMin(f16) },
|
|
// Min denormal value.
|
|
.{ .s = "0x1p-24", .v = math.floatTrueMin(f16) },
|
|
.{ .s = "-0x1p-24", .v = -math.floatTrueMin(f16) },
|
|
};
|
|
|
|
for (cases) |case| {
|
|
try testing.expectEqual(case.v, try parseHexFloat(f16, case.s));
|
|
}
|
|
}
|
|
test "f32" {
|
|
const Case = struct { s: []const u8, v: f32 };
|
|
const cases: []const Case = &[_]Case{
|
|
.{ .s = "0x1p0", .v = 1.0 },
|
|
.{ .s = "-0x1p-1", .v = -0.5 },
|
|
.{ .s = "0x10p+10", .v = 16384.0 },
|
|
.{ .s = "0x10p-10", .v = 0.015625 },
|
|
.{ .s = "0x0.ffffffp128", .v = 0x0.ffffffp128 },
|
|
.{ .s = "0x0.1234570p-125", .v = 0x0.1234570p-125 },
|
|
// Max normalized value.
|
|
.{ .s = "0x1.fffffeP+127", .v = math.floatMax(f32) },
|
|
.{ .s = "-0x1.fffffeP+127", .v = -math.floatMax(f32) },
|
|
// Min normalized value.
|
|
.{ .s = "0x1p-126", .v = math.floatMin(f32) },
|
|
.{ .s = "-0x1p-126", .v = -math.floatMin(f32) },
|
|
// Min denormal value.
|
|
.{ .s = "0x1P-149", .v = math.floatTrueMin(f32) },
|
|
.{ .s = "-0x1P-149", .v = -math.floatTrueMin(f32) },
|
|
};
|
|
|
|
for (cases) |case| {
|
|
try testing.expectEqual(case.v, try parseHexFloat(f32, case.s));
|
|
}
|
|
}
|
|
test "f64" {
|
|
const Case = struct { s: []const u8, v: f64 };
|
|
const cases: []const Case = &[_]Case{
|
|
.{ .s = "0x1p0", .v = 1.0 },
|
|
.{ .s = "-0x1p-1", .v = -0.5 },
|
|
.{ .s = "0x10p+10", .v = 16384.0 },
|
|
.{ .s = "0x10p-10", .v = 0.015625 },
|
|
// Max normalized value.
|
|
.{ .s = "0x1.fffffffffffffp+1023", .v = math.floatMax(f64) },
|
|
.{ .s = "-0x1.fffffffffffffp1023", .v = -math.floatMax(f64) },
|
|
// Min normalized value.
|
|
.{ .s = "0x1p-1022", .v = math.floatMin(f64) },
|
|
.{ .s = "-0x1p-1022", .v = -math.floatMin(f64) },
|
|
// Min denormalized value.
|
|
.{ .s = "0x1p-1074", .v = math.floatTrueMin(f64) },
|
|
.{ .s = "-0x1p-1074", .v = -math.floatTrueMin(f64) },
|
|
};
|
|
|
|
for (cases) |case| {
|
|
try testing.expectEqual(case.v, try parseHexFloat(f64, case.s));
|
|
}
|
|
}
|
|
test "f128" {
|
|
const Case = struct { s: []const u8, v: f128 };
|
|
const cases: []const Case = &[_]Case{
|
|
.{ .s = "0x1p0", .v = 1.0 },
|
|
.{ .s = "-0x1p-1", .v = -0.5 },
|
|
.{ .s = "0x10p+10", .v = 16384.0 },
|
|
.{ .s = "0x10p-10", .v = 0.015625 },
|
|
// Max normalized value.
|
|
.{ .s = "0xf.fffffffffffffffffffffffffff8p+16380", .v = math.floatMax(f128) },
|
|
.{ .s = "-0xf.fffffffffffffffffffffffffff8p+16380", .v = -math.floatMax(f128) },
|
|
// Min normalized value.
|
|
.{ .s = "0x1p-16382", .v = math.floatMin(f128) },
|
|
.{ .s = "-0x1p-16382", .v = -math.floatMin(f128) },
|
|
// // Min denormalized value.
|
|
.{ .s = "0x1p-16494", .v = math.floatTrueMin(f128) },
|
|
.{ .s = "-0x1p-16494", .v = -math.floatTrueMin(f128) },
|
|
.{ .s = "0x1.edcb34a235253948765432134674fp-1", .v = 0x1.edcb34a235253948765432134674fp-1 },
|
|
};
|
|
|
|
for (cases) |case| {
|
|
try testing.expectEqual(@bitCast(u128, case.v), @bitCast(u128, try parseHexFloat(f128, case.s)));
|
|
}
|
|
}
|