[Pitch] Expose attosecond representation of `Duration`

Hello, I recently asked about random durations on this forum. However I was hitting some limitations and was encouraged to pitch. Looking forward to your feedback :slight_smile:

Introduction

This proposal introduces public APIs to enable seamless integration of Int128 into the Duration type. Specifically, it provides support for directly accessing a Duration's attosecond representation via the newly available Int128 type and simplifies the creation of Duration values from attoseconds.

Motivation

The Duration type currently offers two ways to construct and decompose itself:

Low and high bits

public struct Duration: Sendable {
  public var _low: UInt64
  public var _high: Int64
  public init(_high: Int64, low: UInt64) { ... }
}

Components

extension Duration {
  public var components: (seconds: Int64, attoseconds: Int64) { ... }
  public init(secondsComponent: Int64, attosecondsComponent: Int64) { ... }
}

However, both approaches have limitations when it comes to exposing Duration's total attosecond representation:

  • The _low and _high properties are underscored, indicating that their direct use is discouraged.
  • The components property decomposes the value into seconds and attoseconds, requiring additional arithmetic operations for many use cases.

This gap becomes particularly evident in scenarios like generating a random Duration, which currently requires verbose and potentially inefficient code:

func randomDuration(upTo maxDuration: Duration) -> Duration {
  let attosecondsPerSecond: Int128 = 1_000_000_000_000_000_000
  let upperRange = Int128(maxDuration.components.seconds) * attosecondsPerSecond + Int128(maxDuration.components.attoseconds)
  let (seconds, attoseconds) = Int128.random(in: 0..<upperRange).quotientAndRemainder(dividingBy: attosecondsPerSecond)
  return .init(secondsComponent: Int64(seconds), attosecondsComponent: Int64(attoseconds))
}

By introducing direct Int128 support to Duration, this complexity is eliminated. Developers can write concise and efficient code instead:

func randomDuration(upTo maxDuration: Duration) -> Duration {
  return Duration(attoseconds: Int128.random(in: 0..<maxDuration.attoseconds))
}

This addition reduces boilerplate, minimizes potential errors, and improves performance for use cases requiring high-precision time calculations.

Proposed solution

This proposal complements the existing construction and decomposition options by introducing a third approach, leveraging the new Int128 type:

  • A new computed property attoseconds, which exposes the total attoseconds of a Duration as an Int128.
  • A new initializer init(attoseconds: Int128), which allows creating a Duration directly from a single 128-bit value.

These additions provide a direct and efficient mechanism for working with Duration values while maintaining full compatibility with existing APIs.

Detailed design

Internally, the Duration type represents its value using the underscored _high and _low properties, which encode attoseconds as a 128-bit integer split into two 64-bit values. The proposed APIs unify these components into a single Int128 representation:

@available(SwiftStdlib 6.0, *)
extension Duration {
  /// The duration represented in attoseconds.
  public var attoseconds: Int128 {
    Int128(_low: _low, _high: _high)
  }
  
  /// Initializes a `Duration` from the given number of attoseconds.
  public init(attoseconds: Int128) {
    self.init(_high: attoseconds._high, low: attoseconds._low)
  }
}
16 Likes

A couple of points:

The other construction of Duration are done via static methods so it is easy to write .seconds(3) etc. So I would expect that the initialization would actually come in the form of static func attoseconds(_ value: Int128) -> Duration etc.

The var should not be just attoseconds since that could easily be confused with the components. Perhaps instead it should be attosecondsRepresentation etc?

4 Likes

I would very much like to see this implemented! The only reason Duration landed without such operations was that Int128 did not exist as a public type at the time.

I think it would make sense to provide an init(attoseconds:) initializer, as that is in fact the most direct and obvious way to initialize a Duration value, and it would serve as a public substitute for the non-public init(_high:low:) placeholder API that is the current initialization chokepoint.

But I'm not willing to argue this point; I agree it makes sense to provide a static attoseconds method, and I do not mind not exposing a direct initializer for the same.

Hm, on the other hand, I would press on this assertion a little. Do you think Duration.attoseconds would somehow be confusable with components? (Its meaning looks pretty clear to me.) How would attaching some arbitrary suffix remove this confusion?

I specifically remember us leaving room in the Duration API surface to accommodate the eventual advent of Int128; I do not remember us doing that in a way that would require the use of such suffixes. SE-0329 even includes this passage:

If the Swift language gains a signed integer type that can support 128 bits of storage then Duration should be considered to replace the components accessor and initializer with a direct access and initialization to that stored attoseconds value.

8 Likes

Thought about that as well, maybe could‘ve mentioned it in alternatives considered.

My conclusion was that it would not be symmetrical to its siblings (nanoseconds, microseconds, …) because:

  • The Double overload is probably nonsensical because sub-attoseconds are not supported anyways
  • The BinaryInteger overload would again introduce arithmetic because it supports other types than Int128

So it would really just be a one off method which just has one overload with Int128 as a parameter.

Maybe these arguments are not as compelling as I thought after all, but this was the reason I omitted this.

1 Like

Ah, interesting. Thank you for mentioning this.

Oh, gosh, my apologies; I entirely blocked how these factory methods work from my memory. You are right.

The static methods that are generic over BinaryInteger have been a regrettable and expensive mistake (particularly so in the opaque forms in which they originally shipped). I hope we can avoid falling into the same trap again.

We have a clear need to initialize a Duration by simply passing the number of attoseconds as a direct Int128 value, without any fancy generic conversions, any floating point operations, or any other wildly expensive magic tricks. We do need a direct, unambiguous initializer, and init(attoseconds: Int128) is indeed the most obvious way to spell that.

extension Duration {
  @available(SwiftStdlib 6.0, *)
  @_alwaysEmitIntoClient
  public init(attoseconds: Int128) { ... }
}

Whether or not we add any Duration.attoseconds(_:) factories is independent of this. Those are supposed to be convenience extensions, not core API.

For what it's worth, the existing static methods stop at Duration.nanoseconds(_:). That method does not come with a Double-taking variant so neither should anything that goes below that. If @Philippe_Hausler thinks it best to add .attoseconds(some BinaryInteger), then I am not opposed to that, but it would leave a weird gap between atto- and nanoseconds. It might be best to avoid defining any of that.

2 Likes

It might merit pushing back on this for technical reasons.

There are some very weird overload resolution issues with integer literals and generics such that it's possible with some set of overloads but not others that .attoseconds(1_000_000_000_000_000_000_000_000_000_000_000_000) will be inferred as taking a value of default Int type and not compile. This would be a janky experience for no good reason (Int32.max attoseconds is ~2.1 nanoseconds; even Int64.max attoseconds is only ~9.2 seconds).

For all the reasons you mention and this, I prefer the pitched APIs as-is.

3 Likes

Thank you for your feedback so far.

I've added PRs for the formal proposal and the implementation:
https://github.com/swiftlang/swift-evolution/pull/2633
https://github.com/swiftlang/swift/pull/78202

I mentioned the static factory methods in alternatives considered for now.