Lightweight 0.20260625.0
Loading...
Searching...
No Matches
Int128.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include <array>
6#include <cmath>
7#include <compare>
8#include <concepts>
9#include <cstdint>
10#include <string>
11
12namespace Lightweight
13{
14
15// clang-cl doesn't support __int128_t but defines __SIZEOF_INT128__
16// and also since it pretends to be MSVC, it also defines _MSC_VER
17// clang-format off
18#if defined(__SIZEOF_INT128__) && !defined(_MSC_VER)
19 #define LIGHTWEIGHT_HAVE_NATIVE_INT128 1
20#endif
21// clang-format on
22
23namespace detail
24{
25
26 /// Signed 128-bit integer with just enough arithmetic to carry a fixed-point unscaled value.
27 ///
28 /// This exists so that `SqlNumeric`'s unscaled carrier is 128 bits wide on *every* toolchain. Where
29 /// the compiler offers `__int128_t` that type is used directly (see `Int128` below); MSVC and
30 /// clang-cl have no such type, and this software implementation stands in for it there. Without it
31 /// the carrier would fall back to `int64_t`, which holds only 18 decimal digits, and the widest
32 /// declarable precision would differ between toolchains — meaning a `ddl2cpp`-generated record for a
33 /// `money` column (`DECIMAL(19, 4)`) would compile under GCC/Clang and fail under MSVC. A schema is a
34 /// property of the database, not of the compiler that happens to build the client.
35 ///
36 /// @see Int128, SqlMaxNumericPrecision
37 ///
38 /// The operation set is deliberately minimal — construction from and conversion to the floating-point
39 /// types, negation, comparison, and decimal rendering — because that is all `SqlNumeric` needs. It is
40 /// not a general-purpose big integer: there is no multiplication, no division by another `Int128Soft`,
41 /// and no bitwise interface beyond what the conversions require.
42 ///
43 /// Representation is sign-magnitude-free two's complement across a low/high pair, matching the
44 /// little-endian layout of `SQL_NUMERIC_STRUCT::val` so that a `std::memcpy` in either direction is a
45 /// faithful round-trip.
46 struct Int128Soft
47 {
48 /// Low 64 bits of the two's-complement magnitude.
49 std::uint64_t low {};
50
51 /// High 64 bits of the two's-complement magnitude, including the sign bit.
52 std::uint64_t high {};
53
54 /// Default-constructs a zero value.
55 constexpr Int128Soft() noexcept = default;
56
57 /// Constructs from a signed 64-bit integer, sign-extending into the high word.
58 /// @param value Value to widen.
59 constexpr Int128Soft(std::int64_t value) noexcept: // NOLINT(google-explicit-constructor)
60 low { static_cast<std::uint64_t>(value) },
61 high { value < 0 ? ~std::uint64_t { 0 } : std::uint64_t { 0 } }
62 {
63 }
64
65 /// Constructs from an unsigned 64-bit integer, zero-extending into the high word.
66 /// @param value Value to widen.
67 constexpr Int128Soft(std::uint64_t value) noexcept: // NOLINT(google-explicit-constructor)
68 low { value }
69 {
70 }
71
72 /// Constructs from an explicit low/high word pair.
73 /// @param lowWord Low 64 bits.
74 /// @param highWord High 64 bits, including the sign bit.
75 constexpr Int128Soft(std::uint64_t lowWord, std::uint64_t highWord) noexcept:
76 low { lowWord },
77 high { highWord }
78 {
79 }
80
81 /// Constructs from a floating-point value by truncation toward zero.
82 ///
83 /// Values whose magnitude is at or beyond 2^127 saturate to the corresponding extreme rather than
84 /// invoking the undefined behaviour an out-of-range `static_cast` to an integer type would. That
85 /// is a large part of the point of routing through this type: a narrower carrier makes the
86 /// conversion of `money`'s maximum out of range, and on x86-64 that silently flips its sign.
87 ///
88 /// @param value Value to truncate. Must be finite; NaN yields zero.
89 template <std::floating_point T>
90 constexpr explicit Int128Soft(T value) noexcept
91 {
92 if (!(value == value)) // NaN — no meaningful integer image.
93 return;
94
95 auto const negative = value < T { 0 };
96 auto magnitude = negative ? -value : value;
97
98 // 2^127, the first magnitude a signed 128-bit integer cannot represent. Computed by repeated
99 // doubling so it stays exact in every floating-point format (2^127 is a power of two, hence
100 // representable in all of them, but a literal would depend on the type's width).
101 auto limit = T { 1 };
102 for (auto bit = 0; bit != 127; ++bit)
103 limit *= T { 2 };
104
105 if (magnitude >= limit)
106 {
107 // Saturate: -2^127 .. 2^127-1.
108 low = negative ? std::uint64_t { 0 } : ~std::uint64_t { 0 };
109 high = negative ? (std::uint64_t { 1 } << 63) : (~std::uint64_t { 0 } >> 1);
110 return;
111 }
112
113 // Split into two 64-bit halves. `magnitude / 2^64` truncated is the high word; the remainder
114 // is the low word. Both subexpressions are exact: the quotient is < 2^63 and the remainder is
115 // recovered by subtraction rather than by a second division.
116 auto divisor = T { 1 };
117 for (auto bit = 0; bit != 64; ++bit)
118 divisor *= T { 2 };
119
120 auto const highPart = std::trunc(magnitude / divisor);
121 auto const lowPart = std::trunc(magnitude - (highPart * divisor));
122
123 low = static_cast<std::uint64_t>(lowPart);
124 high = static_cast<std::uint64_t>(highPart);
125
126 if (negative)
127 *this = -*this;
128 }
129
130 /// @return `true` if the value is negative.
131 [[nodiscard]] constexpr bool IsNegative() const noexcept
132 {
133 return (high >> 63) != 0;
134 }
135
136 /// Negates the value (two's complement).
137 /// @return The additive inverse. `-(-2^127)` saturates to itself, as it does for every two's-complement type.
138 [[nodiscard]] constexpr Int128Soft operator-() const noexcept
139 {
140 auto const invertedLow = ~low;
141 auto const invertedHigh = ~high;
142 auto const carriedLow = invertedLow + 1;
143 auto const carriedHigh = invertedHigh + (carriedLow < invertedLow ? 1 : 0);
144 return { carriedLow, carriedHigh };
145 }
146
147 /// @return The value unchanged (provided for symmetry with `operator-`).
148 [[nodiscard]] constexpr Int128Soft operator+() const noexcept
149 {
150 return *this;
151 }
152
153 /// Converts to a floating-point type.
154 ///
155 /// Precision is bounded by the target type's significand, exactly as it is for the native
156 /// `__int128_t` this stands in for.
157 ///
158 /// @tparam T Target floating-point type.
159 /// @return The value as `T`, rounded to that type's precision.
160 template <std::floating_point T>
161 [[nodiscard]] constexpr T To() const noexcept
162 {
163 auto const negative = IsNegative();
164 auto const magnitude = negative ? -*this : *this;
165
166 auto scale = T { 1 };
167 for (auto bit = 0; bit != 64; ++bit)
168 scale *= T { 2 };
169
170 auto const result = (static_cast<T>(magnitude.high) * scale) + static_cast<T>(magnitude.low);
171 return negative ? -result : result;
172 }
173
174 /// @return The value as a `float`.
175 [[nodiscard]] constexpr explicit operator float() const noexcept
176 {
177 return To<float>();
178 }
179
180 /// @return The value as a `double`.
181 [[nodiscard]] constexpr explicit operator double() const noexcept
182 {
183 return To<double>();
184 }
185
186 /// @return The value as a `long double`.
187 [[nodiscard]] constexpr explicit operator long double() const noexcept
188 {
189 return To<long double>();
190 }
191
192 /// Narrows to a signed 64-bit integer, truncating the high word.
193 /// @return The low 64 bits reinterpreted as signed, matching `static_cast<int64_t>` on a native `__int128_t`.
194 [[nodiscard]] constexpr explicit operator std::int64_t() const noexcept
195 {
196 return static_cast<std::int64_t>(low);
197 }
198
199 /// Narrows to an unsigned 64-bit integer, truncating the high word.
200 /// @return The low 64 bits, matching `static_cast<uint64_t>` on a native `__int128_t`.
201 [[nodiscard]] constexpr explicit operator std::uint64_t() const noexcept
202 {
203 return low;
204 }
205
206 /// Narrows to a signed 32-bit integer, truncating.
207 [[nodiscard]] constexpr explicit operator std::int32_t() const noexcept
208 {
209 return static_cast<std::int32_t>(low);
210 }
211
212 /// Equality comparison.
213 [[nodiscard]] constexpr bool operator==(Int128Soft const& other) const noexcept = default;
214
215 /// Three-way comparison, signed.
216 /// @param other Value to compare against.
217 /// @return The ordering of `*this` relative to `other`.
218 [[nodiscard]] constexpr std::strong_ordering operator<=>(Int128Soft const& other) const noexcept
219 {
220 if (auto const negative = IsNegative(); negative != other.IsNegative())
221 return negative ? std::strong_ordering::less : std::strong_ordering::greater;
222
223 if (high != other.high)
224 return high <=> other.high;
225
226 return low <=> other.low;
227 }
228 };
229
230 /// Renders a software 128-bit integer in base 10.
231 ///
232 /// `std::format` has no support for 128-bit integers — not even for the native `__int128_t` — so
233 /// decimal rendering is done here by schoolbook long division over 32-bit limbs.
234 ///
235 /// @param value Value to render.
236 /// @return Decimal representation, with a leading '-' for negative values.
237 [[nodiscard]] inline std::string ToDecimalString(Int128Soft value) noexcept
238 {
239 if (value == Int128Soft {})
240 return "0";
241
242 auto const negative = value.IsNegative();
243
244 // Work on the magnitude as an unsigned 128-bit pair. Negating the most-negative value overflows
245 // back to itself, but its two's-complement bit pattern is exactly the unsigned magnitude 2^127,
246 // so treating the words as unsigned below is correct for it too.
247 auto const magnitude = negative ? -value : value;
248 auto low = magnitude.low;
249 auto high = magnitude.high;
250
251 // Divide repeatedly by 10^9, peeling off nine digits per pass. The divisor has to stay below
252 // 2^32: each step of the long division forms `(remainder << 32) | limb`, and that only fits a
253 // std::uint64_t while `remainder < divisor <= 2^32`. (10^19 would be the largest power of ten a
254 // std::uint64_t *holds*, but its remainders overflow that shift — the digits then come out
255 // scrambled, which is exactly what the round-trip tests catch.)
256 constexpr auto chunkDivisor = std::uint64_t { 1'000'000'000ULL };
257 constexpr auto chunkDigits = 9;
258
259 auto chunks = std::string {};
260
261 while (high != 0 || low != 0)
262 {
263 auto remainder = std::uint64_t { 0 };
264
265 auto const limbs = std::array<std::uint32_t, 4> {
266 static_cast<std::uint32_t>(high >> 32),
267 static_cast<std::uint32_t>(high & 0xFFFF'FFFFULL),
268 static_cast<std::uint32_t>(low >> 32),
269 static_cast<std::uint32_t>(low & 0xFFFF'FFFFULL),
270 };
271
272 auto quotientLimbs = std::array<std::uint32_t, 4> {};
273
274 for (auto index = std::size_t { 0 }; index != limbs.size(); ++index)
275 {
276 // `remainder < 10^9 < 2^32`, so `(remainder << 32) | limb` stays within 64 bits.
277 auto const dividend = (remainder << 32) | limbs[index];
278 quotientLimbs[index] = static_cast<std::uint32_t>(dividend / chunkDivisor);
279 remainder = dividend % chunkDivisor;
280 }
281
282 high = (static_cast<std::uint64_t>(quotientLimbs[0]) << 32) | quotientLimbs[1];
283 low = (static_cast<std::uint64_t>(quotientLimbs[2]) << 32) | quotientLimbs[3];
284
285 // Emit this chunk's digits least-significant first; they are zero-padded unless this was the
286 // final (most significant) chunk, whose padding is trimmed after the loop.
287 for (auto digit = 0; digit != chunkDigits; ++digit)
288 {
289 chunks.push_back(static_cast<char>('0' + static_cast<char>(remainder % 10)));
290 remainder /= 10;
291 }
292 }
293
294 // Drop the zero padding of the most significant chunk, then reverse into print order.
295 while (chunks.size() > 1 && chunks.back() == '0')
296 chunks.pop_back();
297
298 if (negative)
299 chunks.push_back('-');
300
301 return std::string { chunks.rbegin(), chunks.rend() };
302 }
303
304} // namespace detail
305
306/// The unscaled carrier `SqlNumeric` keeps `value * 10^Scale` in: a signed 128-bit integer on every
307/// supported toolchain.
308///
309/// Where the compiler provides `__int128_t` that is used directly, so nothing changes for GCC and
310/// Clang. MSVC and clang-cl get `detail::Int128Soft`, a software stand-in with the same width and the
311/// same conversion behaviour. Both are 16 bytes and little-endian, matching `SQL_NUMERIC_STRUCT::val`
312/// byte for byte.
313#if defined(LIGHTWEIGHT_HAVE_NATIVE_INT128)
314using Int128 = __int128_t;
315#else
316using Int128 = detail::Int128Soft;
317#endif
318
319static_assert(sizeof(Int128) == 16, "The unscaled carrier must be exactly as wide as SQL_NUMERIC_STRUCT::val.");
320
321namespace detail
322{
323
324 /// Renders the unscaled carrier in base 10, whichever of the two implementations it is.
325 ///
326 /// `std::format` supports neither `__int128_t` nor `Int128Soft`, so both paths are handled here.
327 ///
328 /// @param value Value to render.
329 /// @return Decimal representation, with a leading '-' for negative values.
330 [[nodiscard]] inline std::string Int128ToString(Int128 value) noexcept
331 {
332#if defined(LIGHTWEIGHT_HAVE_NATIVE_INT128)
333 auto const negative = value < 0;
334 auto magnitude = static_cast<__uint128_t>(negative ? -value : value);
335
336 if (magnitude == 0)
337 return "0";
338
339 auto reversed = std::string {};
340 while (magnitude != 0)
341 {
342 reversed.push_back(static_cast<char>('0' + static_cast<char>(magnitude % 10)));
343 magnitude /= 10;
344 }
345 if (negative)
346 reversed.push_back('-');
347
348 return std::string { reversed.rbegin(), reversed.rend() };
349#else
350 return ToDecimalString(value);
351#endif
352 }
353
354} // namespace detail
355
356} // namespace Lightweight