Skip to content

Conversation

stefanomondino
Copy link

@stefanomondino stefanomondino commented Sep 22, 2025

fixes: #367

As discussed in the issue with @FranzBusch, this is an attempt to allow the new share operator to be available on older Apple platforms (before iOS 18.0 and similar), also known as 1.0 compatibility.

Note: when referring to iOS *** I mean of course all the Apple platforms released in the same year

This comes with a couple of tradeoffs

  1. Mutex had to be replaced by ManagedCriticalState, as it's not available on 1.0
  2. Failure generic type for errors had to be replaced with a non-generic Swift.Error type.
  3. (really minor in my opinion): tests were not compiling on iOS simulators via Xcode with the .milliseconds(10) definitions (iOS 15.0 min compatibility), and I had to switch to nanoseconds. Using older iOS versions was the quickest thing to do on my end to test this scenario.

When ran on iOS 17.5 unfortunately ManagedCriticalState.withLock is crashing for some unknown-to-me reason - I know @phausler already encountered this and I didn't fully understand if there's a possible alternative solution / workaround or not.

I'm opening this here as a starting point.

Also, I was thinking if something this backport of Mutex could help, in terms of approach:
This is only iOS 16.0+ compatible (not 13.0) but this would be a huge step forward for us developers, since it's way easier to have business direction to accept 16.0 as minimum support, compared to 18.0.

This would probably require the entire library to rename 1.1 into 1.2, having 1.1 to be "intermediately" compatible with iOS 16.0 and 1.2 starting from 18.0 - I perfectly understand this is a huge change but I really think that including share (at least this single one!) it's critical for this package adoption.

UPDATE:
Requirements are

  • When using the API in 6.2 it must support proper typed throws
  • When using the API in pre 6.2 it must not be unconditionally always throwing the existental any Error aka Error type
  • When using the API in 6.2 it must funnel through the isolated variation (that is a huge performance hit if not)
  • the versioning of the framework must be 1.1 (if we land this the versioning will then be bumped beyond when requiring newer things); effectively we would be changing the baseline requirements of 1.1 to be a older OS release requirement than the just newly released macOS/iOS etc and 1.2 would then likely be for those requirements.

Additional tasks that would be extra bonus round stuff:

  • Updates to the pitch/documentation
  • Example code in the documentation showing how it is expected to work on pre 6.2

@Akazm
Copy link

Akazm commented Sep 30, 2025

As someone who had quite some issues with Swift stdlibs version constraints (Apple really has a habit of not caring at all, unfortunately), from my experience, packages like this solve this issue quite well:

https://github.com/swhitty/swift-mutex

It's comparable to the backport you mentioned in your Pull Request, but more mature.

@stefanomondino
Copy link
Author

@Akazm I think it's the same (I found it in the one you mentioned at the bottom of the readme, the author is the same).
I've mentioned the gist and not the entire library because it might be easier to integrate in a library like async-algorithms avoiding an explicit third party SPM dependency.

Copy link

@pyrtsa pyrtsa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, would be really useful to have share available for a wide range of OS versions!

Here's a proposed fix for the withLock crash, and a few indent corrections.

Copy link

@pyrtsa pyrtsa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More indentation corrections. 🤔

We'd avoid these if the repo had an .editorconfig file something like:

root = true

[*]
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true

stefanomondino and others added 6 commits October 7, 2025 21:59
Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
@stefanomondino stefanomondino changed the title [WIP] 1.0 compatibility for share operator 1.0 compatibility for share operator Oct 7, 2025
@stefanomondino
Copy link
Author

thanks to @pyrtsa the branch is now passing tests properly on my end.
Waiting for @phausler and @FranzBusch feedback.

@stefanomondino stefanomondino requested a review from pyrtsa October 7, 2025 20:10
Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
Copy link

@pyrtsa pyrtsa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the only thing I wonder is whether we can keep typed throws interface also included under @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)?

There are a few stored properties making it a bit tricky, but I don't yet think it's impossible.

@phausler
Copy link
Member

phausler commented Oct 9, 2025

Just as a heads up, I haven't brushed over this - the implications here are pretty steep so this needs to be carefully considered. Backporting has some severe implications not just to the issues that already are addressed but to the process this package takes beyond just it being hosted here.

long/short please have patience with me while I check on a ton of other stuff (and note - those external requirements may have fallout to the proposed alternative here).

@phausler
Copy link
Member

phausler commented Oct 9, 2025

One thought that might have a better pan-out is leaving this as 1.1 but then altering 1.1 to have an earlier OS requirement and then posing a 1.2 for other algorithms.

@ph1ps
Copy link

ph1ps commented Oct 10, 2025

Having this AsyncSequence throw unconditionally is a deal breaker of this backport IMO.

You would probably need some rethrows trickery to get this working. If this doesn't work I feel like the pitch needs to be updated as well and point that out.

@phausler
Copy link
Member

@stefanomondino per the general concept of back porting share;

Here are the requirements to satisfy -

  • When using the API in 6.2 it must support proper typed throws
  • When using the API in pre 6.2 it must not be unconditionally always throwing the existental any Error aka Error type
  • When using the API in 6.2 it must funnel through the isolated variation (that is a huge performance hit if not)
  • the versioning of the framework must be 1.1 (if we land this the versioning will then be bumped beyond when requiring newer things); effectively we would be changing the baseline requirements of 1.1 to be a older OS release requirement than the just newly released macOS/iOS etc and 1.2 would then likely be for those requirements.

As I discuss more with the parties this potentially impacts this list may grow a bit - but that is a first blush at what would be required to do what you want.

@ph1ps I claim that any destruction of the Never vs any Error throwing case is a non starter for pretty much all algorithms. We have focused a lot of effort to ensure that any non throwing sequence should never require a try for iteration. Any place that doesn't happen it is a destruction of user expectation - so thank you for catching that nuance.

Generally speaking for anyone looking in the future for back ports of things - the bar is REALLY high in pretty much all maintained packages so this should NOT be viewed as a precedent to justify other back ports - they each have to be considered in their own merit, impact, and fallout (it really does pose a lot of complications for maintaining and delivering packages that are blessed as part of the Apple open source side of things and likewise for the Swift core libraries and even more so for the Swift compiler set of targets).

@phausler
Copy link
Member

Additional tasks that would be extra bonus round stuff:

  • Updates to the pitch/documentation
  • Example code in the documentation showing how it is expected to work on pre 6.2

These are not hard requirements for the author to specifically do but anyone who can pitch in that will definitely make my life easier ;)

@stefanomondino
Copy link
Author

stefanomondino commented Oct 11, 2025

Hello, I've tried to update this concept with your suggestions (I completely missed the "forced throw" thing, thank you for catching it).

I've been diving into the matter of this "backportable Failure" for a while, ending up on the forums where the idea seems to be to add a second protocol with a "new" Failure associatedtype. There seems to be some concerns by Slava Pestov about mangled names in distributed libraries (our case), but this should not be our case because - if I understood it correctly - (AsyncAlgorithms,FailableAsyncSequence.Failure) will always follow (_Concurrency,AsyncSequence.Failure) in lexicographic order.

Also, I've set 1.1 to older iOS versions so that we could run tests in both scenarios (i've tried iOS 17.5 and macos 15, they are both working).

Any feedback is welcomed. Thanks.

@stefanomondino
Copy link
Author

Also, on top of that, I don't think we are going to need anything additional documentation for older versions, because it will work as expected - if the underlying sequence never throws, the .share()version won't as well. Otherwise, as with all other sequences, any Errorwill be thrown.

@pyrtsa
Copy link

pyrtsa commented Oct 11, 2025

I think we can conditionally make it support typed throws, not just rethrows. Got a draft implementation working, but AFK right now. Will check back later tonight.

@stefanomondino
Copy link
Author

I think I've found a way as you said, but the iOS 17 crash is back. Will try again later

@stefanomondino
Copy link
Author

"Hic sunt leones" - at least for me.
I've created 1.2 version to allow 1.1 to be ported until ios 16 etc.
I understood where I was failing with the typed error and got it working, but now I'm constantly hitting a runtime error probably related to the missing next() declaration with isolation context on the AsyncIterator when I run tests on iOS 17.5 (it's available via extension only on 1.2).

If I move the next() method inside 1.1 everything works as expected but we lose the typed error on 1.2 (all errors becomes any Error)
At least it's better than forcing non-throwing sequences to handle non-existing errors like before, but still - if the point is to keep typed errors then we need something different.

@pyrtsa
Copy link

pyrtsa commented Oct 15, 2025

@stefanomondino Took a bit longer than I thought to get things to a good shape with a passing unit test, diff to main here: main...pyrtsa:swift-async-algorithms:feature/share-backwards-compatibility.

It's 2 commits behind your current branch because it seems we can reasonably support typed throws by keeping the inner implementation at any Error while forcing the type at the API boundary. This compiles and passes tests on all destinations I could access right now (latest macOS, iOS 15…26).


Here are to @phausler's requests:

Here are the requirements to satisfy -

  • When using the API in 6.2 it must support proper typed throws

Done. There is also a new test which verifies at compile time and run time that the error type is forwarded correctly. (Would've used #366 here if merged!)

Besides, the test was surprisingly hard to write in a way which doesn't coerce the error type to any Error apparently due to shortcomings in compiler support for typed throws. That's why you see a local helper function operation(): inlining its logic in the unstructured Task fails to compile with a type error!

  • When using the API in pre 6.2 it must not be unconditionally always throwing the existental any Error aka Error type
  • When using the API in 6.2 it must funnel through the isolated variation (that is a huge performance hit if not)

I believe these two work correctly now. Where only next() is available, it'll be rethrows. Otherwise, next(isolation:) is preferred by the compiler, passing the isolation actor appropriately.

  • the versioning of the framework must be 1.1 (if we land this the versioning will then be bumped beyond when requiring newer things); effectively we would be changing the baseline requirements of 1.1 to be a older OS release requirement than the just newly released macOS/iOS etc and 1.2 would then likely be for those requirements.

I did this as well now, and I hope I got it right. Ultimately, AsyncAlgorithms 1.1 ended up looking just like AsyncAlgorithms 1.0, only listing visionOS 1.0 at the end of the list is possible. But given that there seems to be virtually no difference, I'm not sure why this was necessary.

  • Updates to the pitch/documentation
  • Example code in the documentation showing how it is expected to work on pre 6.2

These two remain to be done. Can look into it, if you're fine with the general direction.


Lastly, I ended up dropping the AsyncIterator: SendableMetatype requirements, as it complicated the use of the feature in tests and seemed to make no difference to the 6.2 compiler. Please let me know if that wasn't right!

@stefanomondino
Copy link
Author

@pyrtsa that's brilliant, I didn't think of this hack! I've just tried it on my current branch and it's properly working.
I already had a custom "always failing" sequence and I've moved it into the test utilities folder Support folder to be used.

I'm working on the 1.0 / 1.1 / 1.2 thing because the day I tried I had issues without 1.1 being declared, but now it seems to be worthless, you are right.

@stefanomondino
Copy link
Author

@pyrtsa if I set my branch to 1.0 (iOS 13.0 etc) I get a runtime crash on one of the tests (I think the one @phausler was talking about in the original issue, with empty consumers).
However this doesn't happen if I lower 1.1 to be 14.0 (and similar) compatible, which seems a perfect compromise to me.

@pyrtsa
Copy link

pyrtsa commented Oct 15, 2025

However this doesn't happen if I lower 1.1 to be 14.0 (and similar) compatible, which seems a perfect compromise to me.

Great! I only had iOS 15.5 runtime available as the lowest end, so only assumed 13 and 14 would work alike. But 13 seems obsolete to me, and setting AsyncSequence 1.1 to start from 14.0 seems perfect to me too! (Need to do the same treatment to the other OS's as well.)

That'll also mean we do indeed need AsyncSequence 1.1 availability between 1.0 and 1.2.

Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
@stefanomondino
Copy link
Author

BTW - I didn't merge your branch into mine because I thought that having 1.1 and 1.2 was more of a requirement, as stated by Philippe. I just took your suggestion and adapted to my existing changes.
The point: credits for this (if we land this) should definetly go to you, but it's untracked and I didn't want to seem rude or disrespectful - just unexperienced when it comes to this kind of mixed contributions

@pyrtsa
Copy link

pyrtsa commented Oct 15, 2025

Thanks for mentioning, perfectly fine by that! ☺️ Besides, I think your test_share_rethrows_failure_type_without_falling_back_to_any_error is much better at succinctly testing the typed throws behaviour!

//
// This source file is part of the Swift Async Algorithms open source project
//
// Copyright (c) 2022 Apple Inc. and the Swift project authors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright (c) 2022 Apple Inc. and the Swift project authors
// Copyright (c) 2025 Apple Inc. and the Swift project authors

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be some other ones that I copy/pasta'd too .... oops...

// swift-async-algorithms
//
// Created by Stefano Mondino on 15/10/25.
//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be updated to the right attribution header:

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Async Algorithms open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

results1.withLock { $0.append(value) }
// Add some delay to consumer 1
try? await Task.sleep(for: .milliseconds(1))
try? await Task.sleep(nanoseconds: 1_000_000)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worthwhile to keep the test suite requiring a higher version for ease of deployment (and validating the newer versions folks will in time all be using) and it avoids needing to use crusty old versions of the sleep method

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phausler I think it depends on how the general workflow for testing is set up. I think it's useful being able to run tests on legacy platforms if we support them because we found critical issues and crashes there.

I've set AsyncAlgorithms 1.1 to be compatible with iOS 14.0 because - thanks to the tests being able to run there - I discovered issues on iOS 13.0.
I'll create a backportable local extension on Task to avoid the nanoseconds initializer.

XCTAssertEqual(error, TestError.failure)
}
} else {
throw XCTSkip("This test is not available before 1.2")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this can actually hit right?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It certainly can! The overall test suite of this file now passes tests on OS versions as old as iOS 14; it's just this single test case exercising typed throws which really requires iOS 18 or similar, and skipped otherwise.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that XCTest does not support availability macro directly on the function due to how tests are automatically "expanded" from the suite. This means that running this test on older platforms would result in an empty one.
It felt more appropriate to expose it as a skipped test, but it's not a big deal to remove it

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Just a style comment, I think this is cleaner if expressed as guard #available(...) else { throw ... } at the start of the test function.)

// **Cleanup**: Automatically unregisters and cancels pending operations on deinit
final class Side {
// Due to a runtime crash in 1.0 compatible versions, it's not possible to handle
// a generic failure constrained to Base.Failure. We handle inner failure with a `any Error`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// a generic failure constrained to Base.Failure. We handle inner failure with a �`any Error`
// a generic failure constrained to Base.Failure. We handle inner failure with a `any Error`

@jeffhodsdon
Copy link

Generally speaking for anyone looking in the future for back ports of things - the bar is REALLY high in pretty much all maintained packages so this should NOT be viewed as a precedent to justify other back ports - they each have to be considered in their own merit, impact, and fallout (it really does pose a lot of complications for maintaining and delivering packages that are blessed as part of the Apple open source side of things and likewise for the Swift core libraries and even more so for the Swift compiler set of targets).

Do we think the swiftlang organization would be a better home for this project? I'm not seeing much Apple resources and time being allocated here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New 1.1 operators like .share() won't be available on older Apple platforms (eg: iOS <18)

6 participants