← Research index
ORIGINAL RESEARCH5 September 2026

The Fast Path Put a Complex Number in the Wrong Quadrant: A Swift Numerics Sign Audit

A large-negative-real fast path flips component signs in complex cosh and sinh, propagates into cos and sin, and is reinforced by incorrect test expectations.

Khamit Kadyrbekov
Xamit KadirbekovAuthors · GERO Research
Numerical computingSwift NumericsMetamorphic testing
STATUS · AUTHOR'S ANALYSISArguments and proposals are distinguished from the linked laboratories' reported results.

A numerical fast path can preserve magnitude, avoid overflow, and still return the wrong answer.

In Swift Numerics, the large-argument paths for complex `cosh` and `sinh` return the wrong sign for one component when the real part is sufficiently large and negative. Because complex `cos` and `sin` are defined through those hyperbolic functions, the same error propagates into the upper half of the complex plane.

This is not a disagreement about one unit in the last place. The returned value is placed in the wrong quadrant.

A four-call reproducer

import ComplexModule

typealias C = Complex<Double>

print(C.cosh(C(-40, 0.5)))
print(C.sinh(C(-40, 0.5)))
print(C.cos(C(0.5, 40)))
print(C.sin(C(0.5, 40)))

Against Swift Numerics commit `899af71c0256d0ad181e3b7eb3453c1065d928a5`, the observed results are:

cosh(-40 + 0.5i) = ( 1.032850027510405e+17,  5.642485416641618e+16)
sinh(-40 + 0.5i) = (-1.032850027510405e+17, -5.642485416641618e+16)
cos( 0.5 + 40i)  = ( 1.032850027510405e+17,  5.642485416641618e+16)
sin( 0.5 + 40i)  = (-5.642485416641618e+16,  1.032850027510405e+17)

The expected signs are:

cosh(-40 + 0.5i) = ( 1.032850027510405e+17, -5.642485416641618e+16)
sinh(-40 + 0.5i) = (-1.032850027510405e+17,  5.642485416641618e+16)
cos( 0.5 + 40i)  = ( 1.032850027510405e+17, -5.642485416641618e+16)
sin( 0.5 + 40i)  = ( 5.642485416641618e+16,  1.032850027510405e+17)

Two exact symmetries settle the signs

For `z = x + iy`, the defining identities are:

cosh(z) = cosh(x) cos(y) + i sinh(x) sin(y)
sinh(z) = sinh(x) cos(y) + i cosh(x) sin(y)

Since real `cosh` is even and real `sinh` is odd, they imply:

cosh(-x + iy) = conjugate(cosh(x + iy))
sinh(-x + iy) = -conjugate(sinh(x + iy))

The observed values violate both identities by changing the wrong component sign.

This gives a particularly clean oracle: evaluate the same library at `40 + 0.5i`, where its positive-real fast path has the correct sign structure, and apply the exact symmetries. No delicate reference rounding is needed to determine the expected quadrant.

An independent high-precision check

I also evaluated the same input using `mpmath` at 80 decimal digits:

cosh(-40 + 0.5i)
= 103285002751040493.884940309205... - 56424854166416174.631995251979...i

sinh(-40 + 0.5i)
= -103285002751040493.884940309205... + 56424854166416174.631995251979...i

The independent result confirms the symmetry oracle and the expected signs.

Where the fast path goes wrong

For large `abs(x)`, Swift Numerics avoids premature overflow by replacing the large hyperbolic factors with scaled forms based on `exp(abs(x))/2`.

That magnitude approximation is appropriate, but negative `x` requires asymmetric sign handling:

  • in `cosh(x + iy)`, only the imaginary component contains the odd factor `sinh(x)`;
  • in `sinh(x + iy)`, only the real component contains the odd factor `sinh(x)`.

The current large-argument `cosh` path applies no sign correction. The corresponding `sinh` path applies the sign of `x` to the entire complex value. Each therefore changes the wrong component when `x` is negative.

For `Double`, the fast path starts near:

abs(real) >= -log(Double.ulpOfOne) ≈ 36.04

The input `-40 + 0.5i` is finite, ordinary, and comfortably inside that branch.

Why four public functions are affected

The implementation defines:

cos(z) = cosh(iz)
sin(z) = -i sinh(iz)

For `z = 0.5 + 40i`, the transformed hyperbolic input has real part `-40`. The same faulty negative-real branch is therefore used. `cos` receives the wrong imaginary sign, while `sin` receives the wrong real sign.

Why the tests still pass

The existing near-overflow tests do exercise negative real inputs. However, their expected signs match the implementation rather than the mathematical identities:

  • `cosh(-x + iπ/4)` is expected to have a positive imaginary component, but it should be negative;
  • `sinh(-x + iπ/4)` is expected to have a negative imaginary component, but it should be positive.

This is an important testing pattern: a test can cover the right branch and still certify the wrong behavior when its oracle repeats the implementation's sign assumption.

A stronger regression suite would include symmetry or metamorphic checks in addition to fixed expected values.

A bounded fix direction

Let `sx` be `+1` or `-1` according to the sign of the real component. In the scaled branches, the phase factors should have the form:

cosh: (cos(y), sx · sin(y))
sinh: (sx · cos(y), sin(y))

The two-step overflow-avoiding scaling can remain unchanged. The negative-real test expectations then need the corresponding component-sign corrections, with additional symmetry tests preventing regression.

Scope and boundary

This report establishes a reproducible functional error for finite complex inputs in the large-negative-real fast path and its propagation to two derived functions. It is not a security claim, and it does not estimate how often applications supply inputs in the affected region.

The public audit contains the executable Swift reproducer, recorded outputs, source links, independent-oracle values, and the proposed fix direction:

The defect has been reported upstream as Swift Numerics issue #347:

Tested on Apple Swift 6.1.2, arm64 macOS. Independent reproduction and analysis by Xamit Kadirbekov / GERO. No affiliation with Apple or the Swift Numerics maintainers is claimed.