v2 / vlib / encoding / cbor / half.v
114 lines · 108 sloc · 3.45 KB · 468855eef1db0ff73c62be2d1bf176ffa0e1478e
Raw
1module cbor
2
3import math
4
5// Half-precision (binary16) <-> f32/f64 conversion, integer-only.
6// CBOR major type 7, additional info 25 carries an IEEE 754 binary16 value.
7// V has no f16 type, so we synthesise the conversion via bit manipulation.
8//
9// Layout reminder (big-endian on the wire):
10// binary16: 1 sign bit | 5 exponent bits (bias 15) | 10 mantissa bits
11// binary32: 1 sign bit | 8 exponent bits (bias 127) | 23 mantissa bits
12// binary64: 1 sign bit | 11 exponent bits (bias 1023) | 52 mantissa bits
13
14// IEEE 754 binary16 special-value bit patterns — used by the encoder when
15// emitting NaN / ±Inf, and recognised by the decoder. The CBOR canonical
16// quiet-NaN payload is 0x7e00 (RFC 8949 §3.3 / §4.2.2).
17const half_qnan_bits = u16(0x7e00)
18const half_pos_inf_bits = u16(0x7c00)
19const half_neg_inf_bits = u16(0xfc00)
20
21// half_to_f64 expands a 16-bit IEEE 754 value (as a u16) to an f64. Inf and
22// NaN are preserved as the corresponding f64 representations; subnormals
23// are converted exactly.
24@[inline]
25fn half_to_f64(h u16) f64 {
26 sign := u64(h & 0x8000) << 48
27 exp := int((h >> 10) & 0x1f)
28 mant := u64(h & 0x3ff)
29 if exp == 0 {
30 if mant == 0 {
31 return math.f64_from_bits(sign) // ±0
32 }
33 // Subnormal binary16: value = mant * 2^-24. Renormalize for f64.
34 mut m := mant
35 mut e := 1
36 for m & 0x400 == 0 {
37 m <<= 1
38 e++
39 }
40 m &= 0x3ff
41 // Unbiased exp16 = 1 - 15 - (e - 1) = -14 - (e - 1) = -13 - e ; biased f64 = exp + 1023
42 f64_exp := u64(1023 - 14 - (e - 1)) << 52
43 return math.f64_from_bits(sign | f64_exp | (m << 42))
44 }
45 if exp == 0x1f {
46 // Inf or NaN.
47 f64_exp := u64(0x7ff) << 52
48 return math.f64_from_bits(sign | f64_exp | (mant << 42))
49 }
50 // Normal binary16.
51 f64_exp := u64(exp - 15 + 1023) << 52
52 return math.f64_from_bits(sign | f64_exp | (mant << 42))
53}
54
55// f32_to_half tries to round-trip a binary32 value into binary16. Returns
56// (bits, true) when the conversion is exact (lossless), otherwise the
57// boolean is false. NaN is mapped to the canonical quiet NaN 0x7e00 and
58// reported as exact, since the CBOR preferred-serialisation rule
59// (RFC 8949 §4.2.2) authorises that mapping for any NaN payload.
60@[inline]
61fn f32_to_half(v f32) (u16, bool) {
62 bits := math.f32_bits(v)
63 sign := u16((bits >> 16) & 0x8000)
64 exp32 := int((bits >> 23) & 0xff)
65 mant32 := bits & 0x7fffff
66 // Zero.
67 if exp32 == 0 && mant32 == 0 {
68 return sign, true
69 }
70 // Inf.
71 if exp32 == 0xff {
72 if mant32 == 0 {
73 return sign | 0x7c00, true
74 }
75 // NaN: collapse to canonical quiet NaN.
76 return sign | 0x7e00, true
77 }
78 // Real value with unbiased exponent.
79 exp_real := exp32 - 127
80 if exp_real > 15 {
81 return 0, false // would overflow to ±inf, not lossless
82 }
83 if exp_real >= -14 {
84 // Normal range in binary16: low 13 bits of f32 mantissa must be zero.
85 if mant32 & 0x1fff != 0 {
86 return 0, false
87 }
88 half_exp := u16(u32(exp_real + 15) << 10)
89 return sign | half_exp | u16(mant32 >> 13), true
90 }
91 if exp_real >= -24 {
92 // Subnormal in binary16. Build the implicit-leading-1 mantissa and
93 // check the dropped low bits are all zero.
94 shift := u32(-exp_real - 1)
95 full := u32(mant32 | (u32(1) << 23))
96 mask := (u32(1) << shift) - 1
97 if full & mask != 0 {
98 return 0, false
99 }
100 return sign | u16(full >> shift), true
101 }
102 return 0, false
103}
104
105// f64_to_half_via_f32 returns half bits for an f64 only when both
106// f64 -> f32 and f32 -> f16 are lossless.
107@[inline]
108fn f64_to_half(v f64) (u16, bool) {
109 f := f32(v)
110 if f64(f) != v {
111 return 0, false
112 }
113 return f32_to_half(f)
114}
115