How to use `AsyncSequence` on macOS 14.5 in Xcode 16 beta? Need help with availability check since `Failure` is unavailb.e

(I'm using Xcode 16 beta on macOS 14.5)

How do I get this to compile for both macOS 15 and macOS 14? :

public final class EmitterOfEvents {
	public typealias Element = Int
	public func notifications() -> some AsyncSequence<Element, Never> {
		AsyncStream<Element>.makeStream().stream
	}
}

I get error:

I'm well aware of SE-0421, and I'm excited about the change. Hopefully I'm just a bit tired right now and what I want is possible, but it feels like something is missing?

I cannot use:

	@available(macOS 15.0, *)
	public func notifications() -> some AsyncSequence<Element, Never> {
		AsyncStream<Element>.makeStream().stream
	}

Since the method notifications() must exist and work on macOS 14 too.

I cannot use if #available(macOS 15.0, *) { since that is put inside the body, and my problem is the return type...

I try:

#if swift(>=6.0)
	public typealias Notifications = AsyncSequence<Element, Never>
#else
	public typealias Notifications = AsyncSequence<Element>
#endif

That does not work either:

So Swift version is the "wrong dimension".

I guess I want #if os(macOS 15.0, *), which is discussed here, but that does not exist.

So I try to find some macOS 15 only new SDK but could not find any... so I cannot use the canImport(NewFancyMacOS15OnlyKit) "trick" either. hmm...

This is in an SPM proj btw, can I do something clever in Package.swift, to create some macOS version dependent variable?

So can I use @available and create two different versions of notifications() with different return types? Hmm does not seem to be possible to spell "else" for @available ?

So what to do? :man_shrugging: hopefully I've missed something :)

2 Likes

Existing (but unanswered) topic on this subject.

1 Like

Might #if compiler work? :face_with_monocle: will try tommorow

Have you been able to resolve this issue?

Please try the following solution:

macOS 15's AsyncSequence defines a Failure type, which is not available in earlier versions.

Therefore, we utilize the associatedtype keyword and create a new protocol FailureableAsyncSequence that associates a new Failure type. We make AsyncStream conform to both protocols.

When the AsyncSequence in older systems does not define Failure , it will automatically associate with the Failure type from our new protocol.

Although this does not perfectly solve your problem, it is a feasible solution.

public protocol SomeAsyncSequence: AsyncSequence {
    
}

public protocol FailureableAsyncSequence {
    associatedtype Failure: Error
}

public typealias CustomAsyncSequence = SomeAsyncSequence & FailureableAsyncSequence

extension AsyncStream: CustomAsyncSequence where Element == Int {
    public typealias Failure = Never
}

public final class EmitterOfEvents {
    public typealias Element = Int
    public func notifications() -> some CustomAsyncSequence {
        AsyncStream<Element>.makeStream().stream
    }
}
1 Like

Since I have it in protocol, I've managed to workaround it by just constraining Element as preserving this type was the most important part:

#if swift(>=6.0)
    associatedtype ChangesStream: AsyncSequence<Change, Never>
#else
    associatedtype ChangesStream: AsyncSequence where ChangesStream.Element == Change
#endif

With some ugly generic on implementation side in that case (yet not that critical) it is working and keeping type information, while still forcing to catch error at usage side.

Anyway, some #if available could've been useful for such cases, especially given that on Apple platforms that happens a lot.

The AsyncExtensions library, which is based on AsyncSequence, encountered a similar issue. The solution also involved using associatedtype and protocol splitting, allowing both the Element and Failure generics to function properly. You can refer to it here:

AsyncSubject.swift

If you want to implement advanced features like #if available, I recommend using Swift Macro. It allows for very complex code processing but requires more sophisticated programming.

1 Like

When using protocol spliting, or declaring Failure as new associated type or type ailas cause protocol witness table crash in runtime which is before swift 6.
So you shouldn't reference the Failure type directly when you actually run in before swift 6 runtime.

Below code do compile, but using it will cause abort or protocol witness table crash.

public protocol TypedAsyncIteratorProtocol<Element, Err>: ~Copyable {
    
    associatedtype Element
    associatedtype Err: Error
    
    // hack for compiler but crash on runtime, you have to remove this line or Crash!
    typealias Failure = Err
    
    @inlinable
    mutating func next(isolation actor: isolated (any Actor)?) async throws(Err) -> Element?

}


public protocol TypedAsyncSequence<Element, Err>:AsyncSequence where AsyncIterator: TypedAsyncIteratorProtocol{


    /// The type of errors produced when iteration over the sequence fails.
    associatedtype Err = AsyncIterator.Err where Err == AsyncIterator.Err

    /// Creates the asynchronous iterator that produces elements of this
    /// asynchronous sequence.
    ///
    /// - Returns: An instance of the `AsyncIterator` type used to produce
    /// elements of the asynchronous sequence.
    @inlinable
    func makeAsyncIterator() -> AsyncIterator
}

I think these issue has some relation to this kind of thing.
AsyncIterator Failure crash

Are you trying to make this code compile with multiple versions of the compiler/SDK, or are you trying to get the snippet to work with the Swift 6 compiler but a deployment target lower than macOS 15?

The main problem with the code as written is that it specifies a some AsyncSequence type. AsyncSequence only started specifying primary associated types Element and Failure in the Swift 6 standard library and therefore you can only spell a some AsyncSequence type in code that both compiles with a Swift 6 compiler and runs on OSes aligned with Swift 6 or later.

If your code needs to be backward compatible with either older SDKs or older runtimes, then refactoring the code to use a concrete type conforming to AsyncSequence instead is your best bet I think:

public struct EventSequence<Element>: AsyncSequence {
  let underlyingStream: AsyncStream<Element>

  public func makeAsyncIterator() -> AsyncStream<Element>.AsyncIterator {
    underlyingStream.makeAsyncIterator()
  }
}

public final class EmitterOfEvents {
  public typealias Element = Int

  // no need for @available or #if
  public func notifications() -> EventSequence<Element> {
    let stream = AsyncStream<Element>.makeStream().stream
    return EventSequence(underlyingStream: stream)
  }
}

The latter. Which I think is situation many Swift devs are in: Not ready to install macOS 15 beta, but keen on using Xcode 16 beta.

2 Likes

Ok, code that runs on a deployment target lower than macOS 15 can’t use a some AsyncSequence type.

This seems to compile from 6.0 and deploys (runs) on macOS 14.

public protocol EmitterProtocol<Element, Failure> {
  associatedtype Element where Self.Sequence.Element == Element
  @available(macOS 15.0, *) associatedtype Failure : Error = any Error where Self.Sequence.Failure == Failure
  associatedtype Sequence : AsyncSequence
  
  func notifications() -> Sequence
}

public final class Emitter : EmitterProtocol {
  let (stream, continuation) = AsyncStream<Int>.makeStream()
  
  public func notifications() -> AsyncStream<Int> {
    self.stream
  }
}

public final class Listener {
  public func listen(to emitter: some EmitterProtocol) async throws {
    try await self.listen(to: emitter.notifications())
  }
  
  func listen<S>(to sequence: S) async throws where S : AsyncSequence {
    for try await value in sequence {
      print(value)
    }
  }
}

func main() async throws {
  let emitter = Emitter()
  emitter.continuation.yield(1)
  emitter.continuation.yield(2)
  emitter.continuation.yield(3)
  emitter.continuation.finish()
  
  let listener = Listener()
  try await listener.listen(to: emitter)
}

try await main()

Maybe a workaround from a different POV is to not think about returning an opaque sequence type from a concrete Emitter… what if we return a concrete sequence type from an opaque Emitter?

1 Like

I would avoid this trick if possible. Consider this example:

public protocol P1 {
  //associatedtype Failure
}

public protocol P2 {
  associatedtype Failure
}

public func sameTypeRequirement<T: P1 & P2>(_: T)
    where T.Failure == Never {}

The mangled name of sameTypeRequirement() is

s1u19sameTypeRequirementyyxAA2P1RzAA2P2Rzs5NeverO7FailureAaDPRtzlF

However, if you uncomment the associated type declaration in P1, it changes to

s1u19sameTypeRequirementyyxAA2P1RzAA2P2Rzs5NeverO7FailureAaCPRtzlF

Now imagine that P1 is AsyncSequence and P2 is your protocol. Building the code on the old SDK vs new SDK will change the mangled name of our function, which is a problem if you are building a shared library that you intend on distributing. (EDIT: Unless (<<your module name>>, P2) follows (_Concurrency, AsyncSequence) in lexicographic order; then the mangled name won't change).

1 Like

Just want to add that I was experimenting with this and found a few other instances where this leads to trouble at runtime:

protocol P1 {
    associatedtype Base: AsyncSequence
    @available(iOS 18.0, *)
    associatedtype _Failure = Base.Failure
}
public protocol P2 {
    associatedtype _Failure
    associatedtype Failure = _Failure
}
enum AsyncSequenceTraits<Base: AsyncSequence>: P1, P2 {}
typealias FailureOf<S: AsyncSequence> = AsyncSequenceTraits<S>.Failure

This makes it ostensibly possible to replace any usage of S.Failure with FailureOf<S> — but the kicker is that it allows for constructs such as:

func extractFailureType(of sequence: any AsyncSequence) -> Error.Type {
    func helper<S: AsyncSequence>(_ sequence: S) -> Error.Type {
        FailureOf<S>.self
    }
    return helper(sequence)
}

which is entirely unsafe on Darwin prior to the Fall2024 OSes.

FWIW I did initially think we'd be safe as long as we stuck to using this with where constraints, but anecdotally I've also seen crashes in code like the following (could be related to the lexicographical ordering snag that Slava mentioned?)

func doSomething<S: AsyncSequence>(_ sequence: S) where FailureOf<S> == Never {
  ...
}

In fact, I was able to simplify the two protocols into one:

protocol P1 {
    associatedtype Base: AsyncSequence
    associatedtype Failure

    @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
    associatedtype _Witness = Never where Base.Failure == Failure
}
enum AsyncSequenceTraits<Base: AsyncSequence>: P1 {}
typealias FailureOf<S: AsyncSequence> = AsyncSequenceTraits<S>.Failure

Which I've attached as an MRE to the GitHub issue mentioned some posts up: Swift 6 runtime/Compile Crash with AsyncIteratorProcotol TypedThrow · Issue #74292 · swiftlang/swift · GitHub (though I can create a separate issue if it would help with tracking.)

In fact this is a bug in availability checking. To see why, note that the type AsyncSequenceTraits<S>.Failure resolves to a type alias member of AsyncSequenceTraits. This type alias is synthesized when we check the conformance of AsyncSequenceTraits to P1 or P2. The underlying type of this type alias is always going to be Base.Failure, so we can replace AsyncSequenceTraits<S>.Failure with S.Failure. There are no other usages of P1 and P2, so they can be removed.

If you do that though, you get an availability error as expected:

typealias FailureOf<S: AsyncSequence> = S.Failure

To see that there is no other difference, take your original FailureOf and try compiling this program with -emit-silgen:

func f<S: AsyncSequence>(_: FailureOf<S>, _: S) {}

You'll see that the parameter type in both cases is really just S.Failure:

sil hidden [ossa] @$s1a1fyy7FailureQz_xtSciRzlF : $@convention(thin) <S where S : AsyncSequence> (@in_guaranteed S.Failure, @in_guaranteed S) -> () {

That is to say, it's all ultimately boils down to:

func f<S: AsyncSequence>(_: S.Failure, _: S) {}
2 Likes