Skip to content

1.3.0

Latest

Choose a tag to compare

@SintraWorks SintraWorks released this 28 Sep 09:12

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 overflows Int but 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 outgrow Int across two
    words. Every result that did not trap before is unchanged, sign placement included, except one
    that lands on Int.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 returned Int.min/-1, and adding 1 to that trapped.
  • An integer operand of Int.min now works wherever the result fits. Dividing by it, and
    Int.min - fraction and Int.min / fraction, went through the failable initializer, which
    rejects Int.min, and so crashed on a force unwrap whatever the result.
  • nonZeroDivide(by:reducing:) taking an Int ignored reducing and always reduced. The
    non-mutating nonZeroDividing(by:reducing:) was not affected.
  • init(float:significantDigits:) accepted up to 18 digits everywhere, but where Int is 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 of Int.
  • init(float:significantDigits:), and with it every float literal, folded the whole part into the
    numerator as wholes · 10^n before reducing, which overflowed for values as small as 1e15:
    let x: Fraction = 1e15 crashed. It now traps only on a value whose result does not fit.
  • Decoding a Fraction from a plain number too large for one, or from NaN, crashed the process
    doing the decoding. It now throws DecodingError.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 to pow, and so could not build on Linux. It now needs only the standard
    library.
  • power(of:) trapped on an exponent of Int.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

  • Fraction is now a typealias for Rational<Int>, a fraction generic over the integer type of its
    numerator and denominator. Code written against Fraction compiles unchanged. What can tell the
    difference: String(describing: Fraction.self) reads Rational<Int>; FractionError is a
    top-level type, still reachable as Fraction.FractionError; and
    defaultSignificantFloatingPointDigits is 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
    made reduce() 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 a Set 4 to 5 times, and comparing and sorting about twice.

Added

  • Fraction128, a fraction whose numerator and denominator are Int128s: up to 127 bits each,
    where a Fraction's hold 63, and otherwise the same API. It needs Int128, 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
    a Fraction does, and comparing twice as much; results well past 64 bits cost six to seven times
    as much as everyday Fraction arithmetic, as 128-bit division runs in software.
  • init(_:) and init?(exactly:), converting between Fraction and Fraction128, or any two
    widths of Rational: with the fields as written when they fit, and otherwise in lowest terms.
    init(_:) traps where init?(exactly:) returns nil.
  • 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. A Fraction encodes exactly as it did, and a Fraction128 holding
    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 any Fraction128:
    PropertyListEncoder cannot encode an Int128 itself.
  • maximumSignificantFloatingPointDigits, the upper bound of significantDigits: 18 where Int is
    64 bits wide, 9 where it is 32, and 38 for a Fraction128. The default,
    defaultSignificantFloatingPointDigits, stays 4 wherever 10^4 fits, and is this bound where it
    does not, as for a Rational<Int8>.
  • Arithmetic in the FractionsBenchmarks target: +, + 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 for Fraction128, 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 in Int128, at 8, 16, 32 and 64 bits: 40,000 seeded pairs per
    operation for Fraction, and 10,000 at each narrower width, where nearly every operation meets an
    edge. Fraction128 is 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.min traps in the operation that produces it, rather than in a later one.
  • nonZeroDivide(by:reducing:) with an Int and reducing: false no longer reduces.
  • Decoding a keyed payload with a bad field throws that field's error, rather than a typeMismatch
    for Double.

Full changelog: 1.2.0...1.3.0