authorgravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2021-04-28 21:01:15+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-04-28 17:36:12-04:00
log55c58f226d06d3708a82878a67f3800e0ce5810b
treeb32d72d2c4820a346c7dac1e3bd85a4dcc784657
parent86e129eff65f209285e3a3c8430beb41c5acb6cd

std: Add a parser for hexadecimal floating point numbers

Should be good enough to unblock progress on the stage2 compiler. Unifying this parser and the regular one (and perhaps rewrite it, #2207) is left as an exercise for the reader.

2 files changed, 355 insertions(+), 1 deletions(-)

lib/std/fmt.zig+3-1
......@@ -1506,9 +1506,11 @@ test "parseUnsigned" {
15061506}
15071507
15081508pub const parseFloat = @import("fmt/parse_float.zig").parseFloat;
1509pub const parseHexFloat = @import("fmt/parse_hex_float.zig").parseHexFloat;
15091510
1510test "parseFloat" {
1511test {
15111512 _ = @import("fmt/parse_float.zig");
1513 _ = @import("fmt/parse_hex_float.zig");
15121514}
15131515
15141516pub fn charToDigit(c: u8, radix: u8) (error{InvalidCharacter}!u8) {
lib/std/fmt/parse_hex_float.zig created+352
......@@ -0,0 +1,352 @@
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2015-2021 Zig Contributors
3// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
4// The MIT license requires this copyright notice to be included in all copies
5// and substantial portions of the software.const std = @import("std");
6//
7// The rounding logic is inspired by LLVM's APFloat and Go's atofHex
8// implementation.
9
10const std = @import("std");
11const ascii = std.ascii;
12const fmt = std.fmt;
13const math = std.math;
14const testing = std.testing;
15
16const assert = std.debug.assert;
17
18pub fn parseHexFloat(comptime T: type, s: []const u8) !T {
19 assert(@typeInfo(T) == .Float);
20
21 const IntT = std.meta.Int(.unsigned, @typeInfo(T).Float.bits);
22
23 const mantissa_bits = math.floatMantissaBits(T);
24 const exponent_bits = math.floatExponentBits(T);
25
26 const sign_shift = mantissa_bits + exponent_bits;
27
28 const exponent_bias = (1 << (exponent_bits - 1)) - 1;
29 const exponent_min = 1 - exponent_bias;
30 const exponent_max = exponent_bias;
31
32 if (s.len == 0)
33 return error.InvalidCharacter;
34
35 if (ascii.eqlIgnoreCase(s, "nan")) {
36 return math.nan(T);
37 } else if (ascii.eqlIgnoreCase(s, "inf") or ascii.eqlIgnoreCase(s, "+inf")) {
38 return math.inf(T);
39 } else if (ascii.eqlIgnoreCase(s, "-inf")) {
40 return -math.inf(T);
41 }
42
43 var negative: bool = false;
44 var exp_negative: bool = false;
45
46 var mantissa: u128 = 0;
47 var exponent: i16 = 0;
48 var frac_scale: i16 = 0;
49
50 const State = enum {
51 MaybeSign,
52 Prefix,
53 LeadingIntegerDigit,
54 IntegerDigit,
55 MaybeDot,
56 LeadingFractionDigit,
57 FractionDigit,
58 ExpPrefix,
59 MaybeExpSign,
60 ExpDigit,
61 };
62
63 var state = State.MaybeSign;
64
65 var i: usize = 0;
66 while (i < s.len) {
67 const c = s[i];
68
69 switch (state) {
70 .MaybeSign => {
71 state = .Prefix;
72
73 if (c == '+') {
74 i += 1;
75 } else if (c == '-') {
76 negative = true;
77 i += 1;
78 }
79 },
80 .Prefix => {
81 state = .LeadingIntegerDigit;
82
83 // Match both 0x and 0X.
84 if (i + 2 > s.len or s[i] != '0' or s[i + 1] | 32 != 'x')
85 return error.InvalidCharacter;
86 i += 2;
87 },
88 .LeadingIntegerDigit => {
89 if (c == '0') {
90 // Skip leading zeros.
91 i += 1;
92 } else if (c == '_') {
93 return error.InvalidCharacter;
94 } else {
95 state = .IntegerDigit;
96 }
97 },
98 .IntegerDigit => {
99 if (ascii.isXDigit(c)) {
100 if (mantissa >= math.maxInt(u128) / 16)
101 return error.Overflow;
102 mantissa *%= 16;
103 mantissa += try fmt.charToDigit(c, 16);
104 i += 1;
105 } else if (c == '_') {
106 i += 1;
107 } else {
108 state = .MaybeDot;
109 }
110 },
111 .MaybeDot => {
112 if (c == '.') {
113 state = .LeadingFractionDigit;
114 i += 1;
115 } else state = .ExpPrefix;
116 },
117 .LeadingFractionDigit => {
118 if (c == '_') {
119 return error.InvalidCharacter;
120 } else state = .FractionDigit;
121 },
122 .FractionDigit => {
123 if (ascii.isXDigit(c)) {
124 if (mantissa < math.maxInt(u128) / 16) {
125 mantissa *%= 16;
126 mantissa +%= try fmt.charToDigit(c, 16);
127 frac_scale += 1;
128 } else if (c != '0') {
129 return error.Overflow;
130 }
131 i += 1;
132 } else if (c == '_') {
133 i += 1;
134 } else {
135 state = .ExpPrefix;
136 }
137 },
138 .ExpPrefix => {
139 state = .MaybeExpSign;
140 // Match both p and P.
141 if (c | 32 != 'p')
142 return error.InvalidCharacter;
143 i += 1;
144 },
145 .MaybeExpSign => {
146 state = .ExpDigit;
147
148 if (c == '+') {
149 i += 1;
150 } else if (c == '-') {
151 exp_negative = true;
152 i += 1;
153 }
154 },
155 .ExpDigit => {
156 if (ascii.isXDigit(c)) {
157 if (exponent >= math.maxInt(i16) / 10)
158 return error.Overflow;
159 exponent *%= 10;
160 exponent +%= try fmt.charToDigit(c, 10);
161 i += 1;
162 } else if (c == '_') {
163 i += 1;
164 } else {
165 return error.InvalidCharacter;
166 }
167 },
168 }
169 }
170
171 if (exp_negative)
172 exponent *= -1;
173
174 // Bring the decimal part to the left side of the decimal dot.
175 exponent -= frac_scale * 4;
176
177 if (mantissa == 0) {
178 // Signed zero.
179 return if (negative) -0.0 else 0.0;
180 }
181
182 // Divide by 2^mantissa_bits to right-align the mantissa in the fractional
183 // part.
184 exponent += mantissa_bits;
185
186 // Keep around two extra bits to correctly round any value that doesn't fit
187 // the available mantissa bits. The result LSB serves as Guard bit, the
188 // following one is the Round bit and the last one is the Sticky bit,
189 // computed by OR-ing all the dropped bits.
190
191 // Normalize by aligning the implicit one bit.
192 while (mantissa >> (mantissa_bits + 2) == 0) {
193 mantissa <<= 1;
194 exponent -= 1;
195 }
196
197 // Normalize again by dropping the excess precision.
198 // Note that the discarded bits are folded into the Sticky bit.
199 while (mantissa >> (mantissa_bits + 2 + 1) != 0) {
200 mantissa = mantissa >> 1 | (mantissa & 1);
201 exponent += 1;
202 }
203
204 // Very small numbers can be possibly represented as denormals, reduce the
205 // exponent as much as possible.
206 while (mantissa != 0 and exponent < exponent_min - 2) {
207 mantissa = mantissa >> 1 | (mantissa & 1);
208 exponent += 1;
209 }
210
211 // There are two cases to handle:
212 // - We've truncated more than 0.5ULP (R=S=1), increase the mantissa.
213 // - We've truncated exactly 0.5ULP (R=1 S=0), increase the mantissa if the
214 // result is odd (G=1).
215 // The two checks can be neatly folded as follows.
216 mantissa |= @boolToInt(mantissa & 0b100 != 0);
217 mantissa += 1;
218
219 mantissa >>= 2;
220 exponent += 2;
221
222 if (mantissa & (1 << (mantissa_bits + 1)) != 0) {
223 // Renormalize, if the exponent overflows we'll catch that below.
224 mantissa >>= 1;
225 exponent += 1;
226 }
227
228 if (mantissa >> mantissa_bits == 0) {
229 // This is a denormal number, the biased exponent is zero.
230 exponent = -exponent_bias;
231 }
232
233 if (exponent > exponent_max) {
234 // Overflow, return +inf.
235 return math.inf(T);
236 }
237
238 // Remove the implicit bit.
239 mantissa &= @as(u128, (1 << mantissa_bits) - 1);
240
241 const raw: IntT =
242 (if (negative) @as(IntT, 1) << sign_shift else 0) |
243 @as(IntT, @bitCast(u16, exponent + exponent_bias)) << mantissa_bits |
244 @truncate(IntT, mantissa);
245
246 return @bitCast(T, raw);
247}
248
249test "special" {
250 testing.expect(math.isNan(try parseHexFloat(f32, "nAn")));
251 testing.expect(math.isPositiveInf(try parseHexFloat(f32, "iNf")));
252 testing.expect(math.isPositiveInf(try parseHexFloat(f32, "+Inf")));
253 testing.expect(math.isNegativeInf(try parseHexFloat(f32, "-iNf")));
254}
255test "zero" {
256 testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0"));
257 testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "-0x0"));
258 testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0p42"));
259 testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "-0x0.00000p42"));
260 testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0.00000p666"));
261}
262
263test "f16" {
264 const Case = struct { s: []const u8, v: f16 };
265 const cases: []const Case = &[_]Case{
266 .{ .s = "0x1p0", .v = 1.0 },
267 .{ .s = "-0x1p-1", .v = -0.5 },
268 .{ .s = "0x10p+10", .v = 16384.0 },
269 .{ .s = "0x10p-10", .v = 0.015625 },
270 // Max normalized value.
271 .{ .s = "0x1.ffcp+15", .v = math.f16_max },
272 .{ .s = "-0x1.ffcp+15", .v = -math.f16_max },
273 // Min normalized value.
274 .{ .s = "0x1p-14", .v = math.f16_min },
275 .{ .s = "-0x1p-14", .v = -math.f16_min },
276 // Min denormal value.
277 .{ .s = "0x1p-24", .v = math.f16_true_min },
278 .{ .s = "-0x1p-24", .v = -math.f16_true_min },
279 };
280
281 for (cases) |case| {
282 testing.expectEqual(case.v, try parseHexFloat(f16, case.s));
283 }
284}
285test "f32" {
286 const Case = struct { s: []const u8, v: f32 };
287 const cases: []const Case = &[_]Case{
288 .{ .s = "0x1p0", .v = 1.0 },
289 .{ .s = "-0x1p-1", .v = -0.5 },
290 .{ .s = "0x10p+10", .v = 16384.0 },
291 .{ .s = "0x10p-10", .v = 0.015625 },
292 .{ .s = "0x0.ffffffp128", .v = 0x0.ffffffp128 },
293 .{ .s = "0x0.1234570p-125", .v = 0x0.1234570p-125 },
294 // Max normalized value.
295 .{ .s = "0x1.fffffeP+127", .v = math.f32_max },
296 .{ .s = "-0x1.fffffeP+127", .v = -math.f32_max },
297 // Min normalized value.
298 .{ .s = "0x1p-126", .v = math.f32_min },
299 .{ .s = "-0x1p-126", .v = -math.f32_min },
300 // Min denormal value.
301 .{ .s = "0x1P-149", .v = math.f32_true_min },
302 .{ .s = "-0x1P-149", .v = -math.f32_true_min },
303 };
304
305 for (cases) |case| {
306 testing.expectEqual(case.v, try parseHexFloat(f32, case.s));
307 }
308}
309test "f64" {
310 const Case = struct { s: []const u8, v: f64 };
311 const cases: []const Case = &[_]Case{
312 .{ .s = "0x1p0", .v = 1.0 },
313 .{ .s = "-0x1p-1", .v = -0.5 },
314 .{ .s = "0x10p+10", .v = 16384.0 },
315 .{ .s = "0x10p-10", .v = 0.015625 },
316 // Max normalized value.
317 .{ .s = "0x1.fffffffffffffp+1023", .v = math.f64_max },
318 .{ .s = "-0x1.fffffffffffffp1023", .v = -math.f64_max },
319 // Min normalized value.
320 .{ .s = "0x1p-1022", .v = math.f64_min },
321 .{ .s = "-0x1p-1022", .v = -math.f64_min },
322 // Min denormalized value.
323 .{ .s = "0x1p-1074", .v = math.f64_true_min },
324 .{ .s = "-0x1p-1074", .v = -math.f64_true_min },
325 };
326
327 for (cases) |case| {
328 testing.expectEqual(case.v, try parseHexFloat(f64, case.s));
329 }
330}
331test "f128" {
332 const Case = struct { s: []const u8, v: f128 };
333 const cases: []const Case = &[_]Case{
334 .{ .s = "0x1p0", .v = 1.0 },
335 .{ .s = "-0x1p-1", .v = -0.5 },
336 .{ .s = "0x10p+10", .v = 16384.0 },
337 .{ .s = "0x10p-10", .v = 0.015625 },
338 // Max normalized value.
339 .{ .s = "0xf.fffffffffffffffffffffffffff8p+16380", .v = math.f128_max },
340 .{ .s = "-0xf.fffffffffffffffffffffffffff8p+16380", .v = -math.f128_max },
341 // Min normalized value.
342 .{ .s = "0x1p-16382", .v = math.f128_min },
343 .{ .s = "-0x1p-16382", .v = -math.f128_min },
344 // // Min denormalized value.
345 .{ .s = "0x1p-16494", .v = math.f128_true_min },
346 .{ .s = "-0x1p-16494", .v = -math.f128_true_min },
347 };
348
349 for (cases) |case| {
350 testing.expectEqual(@bitCast(u128, case.v), @bitCast(u128, try parseHexFloat(f128, case.s)));
351 }
352}