Repository navigation
Fraction128 holds numerators and denominators of up to 127 bits, for results that outgrow a
Fraction. And arithmetic now traps only when its result does not fit: it used to trap as soon as
an intermediate product overflowed Int, however small the answer, so 1/2^40 + 1/2^41 crashed,
although the answer is 3/2^41. Nothing that worked before returns anything different, except two
kinds of result that were wrong, listed under Upgrading.
Fixed
- Addition, subtraction, multiplication and division, of fractions and of integers, no longer trap
when an intermediate value overflowsIntbut the result fits. Each operation still runs as
before; only if that overflows does it work the result out exactly, cancelling common factors
first (Knuth, TAOCP vol. 2, §4.5.1) and carrying the one sum that can outgrowIntacross two
words. Every result that did not trap before is unchanged, sign placement included, except one
that lands onInt.min, below. - A result that lands on
Int.min, which is outside the range, now traps in the operation that
produces it. It used to be returned, only to trap in whatever touched it next:
Fraction(verifiedNumerator: -(1 << 62), verifiedDenominator: -1) - Fraction(verifiedNumerator: 1 << 62, verifiedDenominator: -1)
is 2^63, which does not fit, yet it returnedInt.min/-1, and adding 1 to that trapped. - An integer operand of
Int.minnow works wherever the result fits. Dividing by it, and
Int.min - fractionandInt.min / fraction, went through the failable initializer, which
rejectsInt.min, and so crashed on a force unwrap whatever the result. nonZeroDivide(by:reducing:)taking anIntignoredreducingand always reduced. The
non-mutatingnonZeroDividing(by:reducing:)was not affected.init(float:significantDigits:)accepted up to 18 digits everywhere, but whereIntis 32 bits
wide, as on arm64_32 watchOS, 10 to the power 10 already overflows it: 10 to 18 digits passed the
check and then trapped. The bound now follows the width ofInt.init(float:significantDigits:), and with it every float literal, folded the whole part into the
numerator aswholes · 10^nbefore reducing, which overflowed for values as small as1e15:
let x: Fraction = 1e15crashed. It now traps only on a value whose result does not fit.- Decoding a
Fractionfrom a plain number too large for one, or from NaN, crashed the process
doing the decoding. It now throwsDecodingError.dataCorrupted. - Decoding a keyed payload with a missing or malformed field reported "expected to decode Double
but found a dictionary": any failure was retried as a plain number, and the retry's failure
thrown. It now throws the error that decoding the field raised. - The library imported
math_h, a module Apple's SDKs define but Swift's Linux module map does
not, for a single call topow, and so could not build on Linux. It now needs only the standard
library. power(of:)trapped on an exponent ofInt.min, which it negated, and multiplied once per unit
of the exponent, so even a base of magnitude one, every power of which fits, could not take a
large exponent. It now squares repeatedly, at most two multiplications per bit of the exponent,
and accepts any exponent. Its results are unchanged, field for field.
Changed
Fractionis now a typealias forRational<Int>, a fraction generic over the integer type of its
numerator and denominator. Code written againstFractioncompiles unchanged. What can tell the
difference:String(describing: Fraction.self)readsRational<Int>;FractionErroris a
top-level type, still reachable asFraction.FractionError; and
defaultSignificantFloatingPointDigitsis computed rather than stored, as a generic type cannot
store a static property.- The hot paths are
@inlinable, so a dependent compiles them specialized for its own types.
Without that, generic code called from another module runs unspecialized, which in a prototype
madereduce()16 times slower. In the benchmark, built for release, every operation costs what
it did in 1.2.0, to within 5%, and adding an integer is 10% faster. A debug build specializes
nothing, though, so there a dependent's arithmetic runs 6 to 7 times slower than 1.2.0's,
reducing, hashing and inserting into aSet4 to 5 times, and comparing and sorting about twice.
Added
Fraction128, a fraction whose numerator and denominator areInt128s: up to 127 bits each,
where aFraction's hold 63, and otherwise the same API. It needsInt128, so macOS 15, iOS 18,
watchOS 11, tvOS 18 or visionOS 2, or any Linux. On everyday values it costs 1.2 to 1.4 times what
aFractiondoes, and comparing twice as much; results well past 64 bits cost six to seven times
as much as everydayFractionarithmetic, as 128-bit division runs in software.init(_:)andinit?(exactly:), converting betweenFractionandFraction128, or any two
widths ofRational: with the fields as written when they fit, and otherwise in lowest terms.
init(_:)traps whereinit?(exactly:)returnsnil.- Each field is encoded as a number when it fits in 64 bits, and as a decimal string when it does
not, and decodes from either. AFractionencodes exactly as it did, and aFraction128holding
an everyday value encodes identically, so either type decodes the other's data wherever the
values fit. The strings are what let property lists carry anyFraction128:
PropertyListEncodercannot encode anInt128itself. maximumSignificantFloatingPointDigits, the upper bound ofsignificantDigits: 18 whereIntis
64 bits wide, 9 where it is 32, and 38 for aFraction128. The default,
defaultSignificantFloatingPointDigits, stays 4 wherever 10^4 fits, and is this bound where it
does not, as for aRational<Int8>.- Arithmetic in the
FractionsBenchmarkstarget:+,+ Int,-,*and/on the benchmark
corpus, and+on a corpus whose every sum takes the exact path. The exact path costs about 2.4
times as much as a sum that does not overflow, and runs only where arithmetic used to trap. The
same measurements forFraction128, and on fields of about 60 bits, whose products run past 120. - Property-based tests checking every arithmetic operation, with and without reducing, against the
1.2.0 formulas evaluated exactly inInt128, at 8, 16, 32 and 64 bits: 40,000 seeded pairs per
operation forFraction, and 10,000 at each narrower width, where nearly every operation meets an
edge.Fraction128is checked across its full range against a separate 256-bit check, which
multiplies every result back out and proves every refusal.
Upgrading
Nothing is source-breaking. Four behaviours change, each of them a trap or a bug before:
- Arithmetic that trapped on an intermediate overflow now returns its result.
- A result landing on
Int.mintraps in the operation that produces it, rather than in a later one. nonZeroDivide(by:reducing:)with anIntandreducing: falseno longer reduces.- Decoding a keyed payload with a bad field throws that field's error, rather than a
typeMismatch
forDouble.
Full changelog: 1.2.0...1.3.0