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 either by forum DM or by email. When contacting me directly, please put "[SE-0533]" at the start of 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 to 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?
I appreciate the thought that I can see went into addressing the concerns I raised relative to my own @DeAsync macro. I understand the arguments for not adopting all of my features, but that does leave @Reasync not usable for my purposes, so I personally would not have a use for the macro as currently proposed.
This doesn't seem like a good idea until the macro composability issue is solved. Given it can easily exist as a library until that time, it doesn't make any sense to me to include it with the language itself. And in general I think there should be a moratorium on macro inclusion until the build performance impact is fully solved. Toolchain macros are better in that regard but still inefficient, so please don't continue to compound this issue until it can be solved.
I appreciate the effort the author has put into this proposal, however I do not believe it is the right path forward.
One of my (not to toot my own horn here) earliest experiments with macros, while said feature was still under development, was a @reasync macro of this form, which I shared with colleagues in an internal repo. The transformation to strip async and await keywords is itself straightforward, but as often as not will produce an invalid synchronous function.
I believe a reasync keyword (à larethrows), or some similar compiler-level functionality/transformation, would be a better way to expose this feature as it would have the full power of the type checker, concurrency checker, and other compiler features to draw on.
The macro targets the population of functions that are asynchronous solely because of their async closure parameters. For that population, removing async and await produces a valid synchronous function by construction. For a function that is asynchronous for other reasons, the generated overload fails to compile, and the compiler reports the error at the invalid expression in the expanded source (there is no silent miscompilation).
The "Semantic validity" section covers this, and the "Diagnosing misuse syntactically at the expansion site" subsection explains why a syntactic check at expansion time cannot reliably reject such misuse in advance.
A language-level reasync would have a real advantage the macro cannot match: type-checker enforcement that a function's async effect arises only from its closure parameters.
The proposal also discusses how that advantage is outweighed by the macro's benefits, drawing on SE-0296 and the Language Steering Group's evaluation of this proposal:
Efficiency: Unlike throwing and non-throwing functions, synchronous and async functions cannot share an ABI entry point, so the polymorphism benefit that makes rethrows cheap does not transfer to reasync (SE-0296).
Applicability: The correct async implementation is often one that is concurrent rather than using a sequential await. Per SE-0296, "reasync is likely to be much less generally applicable than rethrows."
Evolvability: A @Reasync function can later be replaced by a hand-written synchronous overload without disturbing API or ABI; per the LSG, "a macro potentially offers more flexibility to evolve in response to unanticipated needs."
Implementation: Because of that same calling-convention asymmetry, even a language-level reasync would, in the LSG's words, "need to have a macro-like underlying implementation, generating two separate machine-level functions." The duplication exists regardless of where the feature lives.
These points are developed in full in the "Alternatives considered" section. The proposal's position is that the macro is the preferred approach on these merits, not a stopgap pending a language feature. A future reasync proposal remains possible, but its acceptance would not be a prerequisite for solving the common case the macro addresses.
I believe that's one of my points. The set of functions where this transformation is correct is a significant, but not overwhelmingly large, subset of functions it can be applied to, but it is not possible to offer compile-time checking prior to expansion and so developers will be handed failed-macro-expansion diagnostics which are typically hard to read and understand. The developer experience anywhere but the golden path is poor.
As much as anybody on these forums, I am often found roaming the halls of the Apple campus, muttering to myself aphorisms like "the perfect is the enemy of the good". However, I don't think @reasync as proposed rises to the level of "sufficiently good to substitute for perfect".
In any event, I've said my piece and shall go back to hall-roaming and aphorism-muttering.
I'm a -0.5 on this. Whilst is produces some benefits, there are too many edge cases (of which we've hit many in Vapor - the compiler still can't tell the difference between a function with a sync and async function and pick the right one even with awaits in the closure body unless you make it super explicit, which just leads to a bad and confusing developer experience).
With those reasons, I don't see the justification for incorporating this into the standard library instead of just having it as a separately library. The macro works the same (i.e. it's not required to be in the standard library or it wouldn't work otherwise).
There is some existing overload-resolution friction in Swift, but that friction is a result of having synchronous and asynchronous overloads of the same function in general, not of how those overloads are produced. The "Semantic validity" section makes the point that any property of the generated peer is a property of the equivalent hand-written declaration.
The macro generates an ordinary overload pair, identical to one written by hand, and relies on the existing resolution rule from SE-0296. The "Overload resolution" section explains that the macro introduces no new resolution behavior. The cases where the compiler needs an explicit annotation to disambiguate, including resolution inside a closure body, arise identically for any hand-written sync/async pair.
Agreed that the macro does not require standard-library inclusion to function. The proposal does not call for standard-library inclusion specifically; it leaves the venue open between the standard library and an official package in the swiftlang organization (the latter being a canonical library rather than part of the standard library itself).
The case for a canonical home in either venue is to prevent fragmentation. This is discussed in the subection "Keeping the macro as a third-party package". The duplication problem the macro solves is common across libraries that expose both calling conventions. If left as a third-party solution, each adopting library either depends on one particular package or reimplements the transformation, and the ecosystem accumulates multiple incompatible versions with inconsistent naming, semantics, and diagnostics. A canonical location avoids that and signals a single recommended solution.
I still think that while yes, it is a bunch of work to get this working as reasync at the compiler and language level, it is worth delaying and doing that work. Otherwise we risk introducing a different kind of fragmentation into the language that is not trivial to explain to newcomers (why throws and rethrows but async and @Reasync?)
That said, it is nice that the proposal leaves the door opening for getting this functionality sooner rather than later through an official package. That seems like the right approach without shutting out the possibility of implementing reasync down the road and offering a fix-it to migrate folks from @Reasync to reasync.
So as it stands, still generally -0.5 in favor of making reasync possible, but providing an official package now seems like on okay way to solve a stalemate if absolutely needed.
I don't feel like this is "good enough" for inclusion in the stdlib.
That's not a knock on the implementation or the effort that went into it! It seems like a good implementation of the concept, as far as macros allow.
It should just be made an official swiftlang-org package, similar to swift-async-algorithms. Then whoever needs it and can work within the limitations, can have it. And that way the community has a focus for improvements, rather than multiple competing possibly-unmaintained implementations.
There is an asymmetry and a small teachability cost, and a reasync keyword would indeed reduce it: throws/rethrows beside async/reasync reads more consistently than a keyword beside a macro.
But the asymmetry reflects a fundamental difference between the two effects. rethrows earns a keyword because throwing and non-throwing functions share a single ABI entry point, so one symbol serves both contexts at no cost (SE-0296). async has a different ABI and cannot share an entry point with its synchronous form, so any reasync implementation must produce two separate functions. The Language Steering Group noted in its evaluation that even an integrated reasync would need a macro-like underlying implementation generating two machine-level functions.
So a reasync keyword would mainly buy surface consistency; it could not give async the single shared entry point that makes rethrows cheap, because the two ABIs differ. The two functions exist either way. The "A language-level reasync" section of the proposal goes into more detail on this.
The fix-it would be a sensible mechanism if that scenario arises. Compatibility and adoption are part of the proposal's intent. Since the macro generates an ordinary overload, an @Reasync function can be replaced at any time by a hand-written synchronous overload without disturbing API or ABI.
Does this support typed throws automatically? I don't see this mentioned in the proposal. It seems a natural addition to this macro, would be great if it does support.
Typed throws are for sure a natural fit, and yes, they're automatically supported. The macro preserves everything by default and removes only async, await, and certain concurrency annotations, so typed throws come through untouched. For example:
I wonder if there’s a missing feature here to allow a macro implementation to modify the diagnostics shown when an expansion fails to compile. Adding function not compatible with ‘@Reasync’; to the beginning of the diagnostic along with a fix-it to remove the attribute might go a long way towards improving its usability.
Sorry for the very late and very very very dumb question.
Might be me not being a native English speaker, but @Reasync reads to me as making something async again. Can you teach me how to better think and teach the logic flow that connects functionality and name please?
This is something @Douglas_Gregor and I have discussed previously as it would help Swift Testing too. So I'd love to see it added, even if @Reasync doesn't move forward.
Adding a feature to the compiler to allow macros to hook into diagnostics would be great. It would target exactly the gap discussed earlier: when a generated peer fails type-checking, the diagnostic itself is an ordinary compiler error, but it appears in the macro expansion rather than at the attachment site. A prefix establishing that context, plus the removal fix-it, would address the part of the user experience that macros cannot currently reach.
This macro would need no design changes to adopt such a hook. It already emits diagnostics with removal fix-its at expansion time (a warning with a fix-it to remove a redundant nested @Reasync), so attaching the same kind of fix-it to post-expansion diagnostics would slot into existing machinery once the compiler exposes the hook.
If the proposal is revised, this seems like a natural Future Directions entry.
Not a dumb question. The name has a lineage that isn't obvious if you haven't followed the older discussions.
The prefix isn't "re-" as in "again". The name is a deliberate parallel to rethrows, where the prefix doesn't mean "throws again" either: a rethrows function is one whose throwing is conditional on its closure arguments. For many years, "reasync" has been the community's name for the same idea applied to async. The macro's mechanism differs, since it generates a synchronous overload rather than making a single declaration conditionally async, but the name describes the user-facing outcome rather than the mechanism: just as rethrows describes a function that exists in both throwing and non-throwing forms, @Reasync describes a function that exists in both asynchronous and synchronous forms.
The "Naming" subsection of the proposal goes into more detail, including the alternative names that were considered.
There's a potential tie-in with the "global defaults" proposal too, where "add a macro to every applicable declaration" is clearly nonsense at the moment, but if the macro could throw a NotApplicableToDeclaration(...) error and have the compiler treat that specially (ignore the attempted application if it came from a global default, produce a nice error if the macro's from the user's source) that would start to become a reasonable expansion of the idea.