Commit 9d409233b2

LemonBoy <thatlemon@gmail.com>
2021-03-23 10:08:34
std: Implement hex float printing
The results have been cross-checked with LLVM's APFloat implementation by randomly sampling the f32/f64 space, while the f16 one was completely checked given the small size.
1 parent 18b4648
Changed files (1)
lib
lib/std/fmt.zig
@@ -696,6 +696,11 @@ fn formatFloatValue(
             error.NoSpaceLeft => unreachable,
             else => |e| return e,
         };
+    } else if (comptime std.mem.eql(u8, fmt, "x")) {
+        formatFloatHexadecimal(value, options, buf_stream.writer()) catch |err| switch (err) {
+            error.NoSpaceLeft => unreachable,
+            else => |e| return e,
+        };
     } else {
         @compileError("Unsupported format string '" ++ fmt ++ "' for type '" ++ @typeName(@TypeOf(value)) ++ "'");
     }
@@ -1023,6 +1028,112 @@ pub fn formatFloatScientific(
     }
 }
 
+pub fn formatFloatHexadecimal(
+    value: anytype,
+    options: FormatOptions,
+    writer: anytype,
+) !void {
+    if (math.signbit(value)) {
+        try writer.writeByte('-');
+    }
+    if (math.isNan(value)) {
+        return writer.writeAll("nan");
+    }
+    if (math.isInf(value)) {
+        return writer.writeAll("inf");
+    }
+
+    const T = @TypeOf(value);
+    const TU = std.meta.Int(.unsigned, std.meta.bitCount(T));
+
+    const mantissa_bits = math.floatMantissaBits(T);
+    const exponent_bits = math.floatExponentBits(T);
+    const mantissa_mask = (1 << mantissa_bits) - 1;
+    const exponent_mask = (1 << exponent_bits) - 1;
+    const exponent_bias = (1 << (exponent_bits - 1)) - 1;
+
+    const as_bits = @bitCast(TU, value);
+    var mantissa = as_bits & mantissa_mask;
+    var exponent: i32 = @truncate(u16, (as_bits >> mantissa_bits) & exponent_mask);
+
+    const is_denormal = exponent == 0 and mantissa != 0;
+    const is_zero = exponent == 0 and mantissa == 0;
+
+    if (is_zero) {
+        // Handle this case here to simplify the logic below.
+        try writer.writeAll("0x0");
+        if (options.precision) |precision| {
+            if (precision > 0) {
+                try writer.writeAll(".");
+                try writer.writeByteNTimes('0', precision);
+            }
+        } else {
+            try writer.writeAll(".0");
+        }
+        try writer.writeAll("p0");
+        return;
+    }
+
+    if (is_denormal) {
+        // Adjust the exponent for printing.
+        exponent += 1;
+    } else {
+        // Add the implicit 1.
+        mantissa |= 1 << mantissa_bits;
+    }
+
+    // Fill in zeroes to round the mantissa width to a multiple of 4.
+    if (T == f16) mantissa <<= 2 else if (T == f32) mantissa <<= 1;
+
+    const mantissa_digits = (mantissa_bits + 3) / 4;
+
+    if (options.precision) |precision| {
+        // Round if needed.
+        if (precision < mantissa_digits) {
+            // We always have at least 4 extra bits.
+            var extra_bits = (mantissa_digits - precision) * 4;
+            // The result LSB is the Guard bit, we need two more (Round and
+            // Sticky) to round the value.
+            while (extra_bits > 2) {
+                mantissa = (mantissa >> 1) | (mantissa & 1);
+                extra_bits -= 1;
+            }
+            // Round to nearest, tie to even.
+            mantissa |= @boolToInt(mantissa & 0b100 != 0);
+            mantissa += 1;
+            // Drop the excess bits.
+            mantissa >>= 2;
+            // Restore the alignment.
+            mantissa <<= @intCast(math.Log2Int(TU), (mantissa_digits - precision) * 4);
+
+            const overflow = mantissa & (1 << 1 + mantissa_digits * 4) != 0;
+            // Prefer a normalized result in case of overflow.
+            if (overflow) {
+                mantissa >>= 1;
+                exponent += 1;
+            }
+        }
+    }
+
+    // +1 for the decimal part.
+    var buf: [1 + mantissa_digits]u8 = undefined;
+    const N = formatIntBuf(&buf, mantissa, 16, false, .{ .fill = '0', .width = 1 + mantissa_digits });
+
+    try writer.writeAll("0x");
+    try writer.writeByte(buf[0]);
+    if (options.precision != @as(usize, 0))
+        try writer.writeAll(".");
+    const trimmed = mem.trimRight(u8, buf[1..], "0");
+    try writer.writeAll(trimmed);
+    // Add trailing zeros if explicitly requested.
+    if (options.precision) |precision| if (precision > 0) {
+        if (precision > trimmed.len)
+            try writer.writeByteNTimes('0', precision - trimmed.len);
+    };
+    try writer.writeAll("p");
+    try formatInt(exponent - exponent_bias, 10, false, .{}, writer);
+}
+
 /// Print a float of the format x.yyyyy where the number of y is specified by the precision argument.
 /// By default floats are printed at full precision (no rounding).
 pub fn formatFloatDecimal(
@@ -1900,6 +2011,54 @@ test "float.special" {
     try expectFmt("f64: -inf", "f64: {}", .{-math.inf_f64});
 }
 
+test "float.hexadecimal.special" {
+    try expectFmt("f64: nan", "f64: {x}", .{math.nan_f64});
+    // negative nan is not defined by IEE 754,
+    // and ARM thus normalizes it to positive nan
+    if (builtin.arch != builtin.Arch.arm) {
+        try expectFmt("f64: -nan", "f64: {x}", .{-math.nan_f64});
+    }
+    try expectFmt("f64: inf", "f64: {x}", .{math.inf_f64});
+    try expectFmt("f64: -inf", "f64: {x}", .{-math.inf_f64});
+
+    try expectFmt("f64: 0x0.0p0", "f64: {x}", .{@as(f64, 0)});
+    try expectFmt("f64: -0x0.0p0", "f64: {x}", .{-@as(f64, 0)});
+}
+
+test "float.hexadecimal" {
+    try expectFmt("f16: 0x1.554p-2", "f16: {x}", .{@as(f16, 1.0 / 3.0)});
+    try expectFmt("f32: 0x1.555556p-2", "f32: {x}", .{@as(f32, 1.0 / 3.0)});
+    try expectFmt("f64: 0x1.5555555555555p-2", "f64: {x}", .{@as(f64, 1.0 / 3.0)});
+    try expectFmt("f128: 0x1.5555555555555555555555555555p-2", "f128: {x}", .{@as(f128, 1.0 / 3.0)});
+
+    try expectFmt("f16: 0x1.p-14", "f16: {x}", .{@as(f16, math.f16_min)});
+    try expectFmt("f32: 0x1.p-126", "f32: {x}", .{@as(f32, math.f32_min)});
+    try expectFmt("f64: 0x1.p-1022", "f64: {x}", .{@as(f64, math.f64_min)});
+    try expectFmt("f128: 0x1.p-16382", "f128: {x}", .{@as(f128, math.f128_min)});
+
+    try expectFmt("f16: 0x0.004p-14", "f16: {x}", .{@as(f16, math.f16_true_min)});
+    try expectFmt("f32: 0x0.000002p-126", "f32: {x}", .{@as(f32, math.f32_true_min)});
+    try expectFmt("f64: 0x0.0000000000001p-1022", "f64: {x}", .{@as(f64, math.f64_true_min)});
+    try expectFmt("f128: 0x0.0000000000000000000000000001p-16382", "f128: {x}", .{@as(f128, math.f128_true_min)});
+
+    try expectFmt("f16: 0x1.ffcp15", "f16: {x}", .{@as(f16, math.f16_max)});
+    try expectFmt("f32: 0x1.fffffep127", "f32: {x}", .{@as(f32, math.f32_max)});
+    try expectFmt("f64: 0x1.fffffffffffffp1023", "f64: {x}", .{@as(f64, math.f64_max)});
+    try expectFmt("f128: 0x1.ffffffffffffffffffffffffffffp16383", "f128: {x}", .{@as(f128, math.f128_max)});
+}
+
+test "float.hexadecimal.precision" {
+    try expectFmt("f16: 0x1.5p-2", "f16: {x:.1}", .{@as(f16, 1.0 / 3.0)});
+    try expectFmt("f32: 0x1.555p-2", "f32: {x:.3}", .{@as(f32, 1.0 / 3.0)});
+    try expectFmt("f64: 0x1.55555p-2", "f64: {x:.5}", .{@as(f64, 1.0 / 3.0)});
+    try expectFmt("f128: 0x1.5555555p-2", "f128: {x:.7}", .{@as(f128, 1.0 / 3.0)});
+
+    try expectFmt("f16: 0x1.00000p0", "f16: {x:.5}", .{@as(f16, 1.0)});
+    try expectFmt("f32: 0x1.00000p0", "f32: {x:.5}", .{@as(f32, 1.0)});
+    try expectFmt("f64: 0x1.00000p0", "f64: {x:.5}", .{@as(f64, 1.0)});
+    try expectFmt("f128: 0x1.00000p0", "f128: {x:.5}", .{@as(f128, 1.0)});
+}
+
 test "float.decimal" {
     try expectFmt("f64: 152314000000000000000000000000", "f64: {d}", .{@as(f64, 1.52314e+29)});
     try expectFmt("f32: 0", "f32: {d}", .{@as(f32, 0.0)});