SE-0552: Rounding of Float.pi

Hello, Swift community!

The review of SE-0552: Rounding of Float.pi begins now and runs through October 6, 2026.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0552" in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

swift-evolution/process.md at main · swiftlang/swift-evolution · GitHub

Thank you,

Doug Gregor
Review Manager

10 Likes

This seems like a obvious pitch to me. +1

I agree that this is the right change to make.

It'd be nice if the proposal document clarified that the other values of pi in the standard library do not need to change. Perhaps something like this at the end of Detailed Design:

The pi values of Swift's other standard floating point types — Float16, Double, and Float80 — are all already correct under the new rule because the closest representable value to π in these types happens to be the one chosen by rounding towards zero. Only Float.pi requires an adjustment.

9 Likes

Feels a bit silly to even formally propose it, but… yes, the rationale makes sense. I would hazard a guess that if a program's correctness is affected by this change, it is already on shaky ground when doing floating-point math.

Ship it.

6 Likes

+1 to the proposal.

Just as @John_McCall suggests an addition to the proposal to clarify the change, I think it would be helpful to developers to mention the potential 'angles in the wrong quadrant' issue for Float values in the updated comment/documentation.

I realize that documentation is not typically part of a review, but a change to the comments/documentation is part of this proposal's Detailed Design.

1 Like

It's worse than that, people don't realize how overspecified it already is for any realistic calculation, not to mention these are platonic forms that never match the real world they are meant to model.

2 Likes

FWIW describing this as Float32 instead of Float would probably be more clear (even though they are the same type).

1 Like

I am +1 on this proposal.

it may be necessary to use 0x1.921fb4p1 instead of Float.pi.

Using the literal value directly may be a little inconvenient for users who write numerical algorithms using BinaryFloatingPoint generics.

I thought about a possible workaround that users could implement themselves:

extension BinaryFloatingPoint {
    static var PI: Self {
        compatPI()
    }

    // Is this necessary?
    // @specialized(where Self == Float16)
    // @specialized(where Self == Float)
    // @specialized(where Self == Double)
    private static func compatPI() -> Self {
        if Self.self == Float.self {
            0x1.921fb4p1
        } else {
            .pi
        }
    }
}

I expect that the type check and the branch can be optimized away when the generic code is specialized, leaving the constant directly in the generated code.

Are there any problems with this approach?

In unspecialized generic code that would have some small cost, but it would be completely swamped by the overall unspecialized generic cost, so yeah, that's totally fine for someone to do.

1 Like

Proposal

In some cases you can get away with using operations that are already defined on FloatingPoint/BinaryFloatingPoint. For example, if the new value of pi makes your operation “overshoot” some range, you can use min/max to correct it.

func foo<F: BinaryFloatingPoint>(…) {
  let x: F = …
  let value = x * F.pi // Operation involving the new pi
  let correctedValue = F.minimum(value, 1.0) // Correction
}

Personally, I do not think that a lot of people will be affected by this change.

Tiny change

There are other implementations of FloatingPoint outside of stdlib. Authors of those implementations need to be notified.

For example, there are a few implementations of IEEE 754 decimals. Without this/pitch thread I would never notice the change, which would make my implementation non-conforming to FloatingPoint.

Documentation changes

I see that the Swift team is currently working on floating points.

A few years ago, I pointed out some inconsistencies in how FloatingPoint protocol is defined, but I have never received any answers. As it currently stands, it is not possible to conform to this protocol, as it contradicts itself.

Can someone look at this? Do we need a new thread?

Minimum with sNaN is ambiguous

For FloatingPoint.minimum(_:_:) the documentation states:

This method returns the minimum of two values, preserving order and eliminating NaN when possible.

This sentence does not specify whether we are talking about quiet or signaling NaN, so it applies to both: NaNs should be eliminated when possible.

For two values x and y, the result of minimum(x, y) is x if x <= y, y if y < x, or whichever of x or y is a number if the other is a quiet NaN.

Both x <= y and y < x are false if one of the operands is NaN. The rest of the sentence says:

  • if one of the operands is quiet NaN -> return number
  • signaling NaN is not mentioned

If both x and y are NaN, or either x or y is a signaling NaN, the result is NaN.

This sentence says that the result is NaN if one of the following is true:

  • both x and y are NaN
  • either x or y is a signaling NaN

Based on the above, what should be the result of min(sNaN, 1.0)?

  • Answer A: 1.0 because “eliminating NaN when possible” takes precedence
  • Answer B: NaN because “either x or y is a signaling NaN, the result is NaN” takes precedence

In swift-6.3.2-RELEASE on x86_64-unknown-linux-gnu the answer A (1.0) would be correct because of the default implementation:

@inlinable
public static func minimum(_ x: Self, _ y: Self) -> Self {
  if x <= y || y.isNaN { return x }
  return y
}

Minimum is a bit weird, as it has changed between IEEE 754 2008 and IEEE 754 2019. The reasoning is described in this document. That said, Swift documentation should be unambiguous.

ScaleB requires NaN equality?

For init(sign:exponent:significand:) the documentation states:

If the value you pass to significand is NaN, the result is NaN.

100% correct, no problems here.

For any floating-point value x of type F, the result of the following is equal to x, with the distinction that the result is canonicalized if x is in a noncanonical encoding:

let x0 = F(sign: x.sign, exponent: x.exponent, significand: x.significand)

If significand is NaN then the result is NaN. In such case the, “is equal to x” guarantee will not be true.

let x = Double.nan
let x0 = Double(sign: x.sign, exponent: x.exponent, significand: x.significand)

print(x) // nan
print(x0) // nan
print(x == x0) // false

FloatingPoint does not conform to AdditiveArithmetic

FloatingPoint types have to also conform to AdditiveArithmetic:

protocol FloatingPoint : Hashable, SignedNumeric, Strideable
protocol SignedNumeric : Numeric
protocol Numeric : AdditiveArithmetic, ExpressibleByIntegerLiteral

For AdditiveArithmetic.zero the documentation states:

Zero is the identity element for addition. For any value, x + .zero == x and .zero + x == x.

This is not true for NaN:

let x = Double.nan
print(x + Double.zero) // nan
print(x + Double.zero == x) // false

I guess Swift just went with the pragmatic choice, but they never documented it.

Incorrect pi value

Slightly off-topic, but famously Doom shipped with incorrect value of pi. And the game still worked. Someone even did a presentation on how different values of pi affect the gameplay (timestamp to the moment where the actual talk starts).

That would be quite unrelated to this proposal.


Operations that are explicitly documented to have IEEE semantics—e.g., minimum is IEEE 754-2008 minNum and init(sign:exponent:significand:) is IEEE 754 scaleB—are intended to document those semantics and any wording that departs from the standard are documentation bugs.

(That said, where minNum is underspecified in IEEE 754-2008, so too would be Swift's corresponding API. So it's important to be clear there whether your concern is with IEEE 754-2008--not modifiable--or with Swift's description of the standard.)

1 Like

SE-0552 has been accepted; on behalf of Doug, thank you all for your contributions during this review.

John McCall
Language Steering Group

2 Likes