Skip to content

Commit 58d5059

Browse files
committed
Sema: Make @concurrent not imply async on closures
The present approach is not prudent because `@concurrent` synchronous functions, a natural extension, are a likely-to-happen future direction, whereas the current inference rule is entirely grounded on `@concurrent` being exclusive to async functions. If we were to ship this rule, we would have to keep the promise for backwards compatibility when implementing the aforementioned future direction, replacing one inconsistency with another, and possibly introducing new bug-prone expression checking code. ```swift func foo(_: () -> Void) {} func foo(_: () async -> Void) {} // In a future without this change and `@concurrent` synchronous // functions accepted, the first call resolves to the first overload, // and the second call resolves to the second, despite `@concurrent` no // longer implying `async`. foo { } foo { @Concurrent in } ``` This change also drops the fix-it for removing `@concurrent` when used on a synchronous closure. With the inference rule gone, and the diagnosis delayed until after solution application, this error raises a fairly balanced choice between removing the attribute and being explicit about the effect, where a unilateral suggestion is quite possibly more harmful than useful.
1 parent dbedb21 commit 58d5059

File tree

5 files changed

+96
-23
lines changed

5 files changed

+96
-23
lines changed

lib/Sema/ConstraintSystem.cpp

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,17 +1450,11 @@ FunctionType::ExtInfo ClosureEffectsRequest::evaluate(
14501450
if (!body)
14511451
return ASTExtInfoBuilder().withSendable(sendable).build();
14521452

1453-
// `@concurrent` attribute is only valid on asynchronous function types.
1454-
bool asyncFromAttr = false;
1455-
if (expr->getAttrs().hasAttribute<ConcurrentAttr>()) {
1456-
asyncFromAttr = true;
1457-
}
1458-
14591453
auto throwFinder = FindInnerThrows(expr);
14601454
body->walk(throwFinder);
14611455
return ASTExtInfoBuilder()
14621456
.withThrows(throwFinder.foundThrow(), /*FIXME:*/Type())
1463-
.withAsync(asyncFromAttr || bool(findAsyncNode(expr)))
1457+
.withAsync(bool(findAsyncNode(expr)))
14641458
.withSendable(sendable)
14651459
.build();
14661460
}

lib/Sema/MiscDiagnostics.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ static void diagSyntacticUseRestrictions(const Expr *E, const DeclContext *DC,
305305
}
306306
}
307307

308+
// Performs checks on a closure.
309+
if (auto *closure = dyn_cast<ClosureExpr>(E)) {
310+
checkClosure(closure);
311+
}
312+
308313
// Specially diagnose some checked casts that are illegal.
309314
if (auto cast = dyn_cast<CheckedCastExpr>(E)) {
310315
checkCheckedCastExpr(cast);
@@ -1455,6 +1460,33 @@ static void diagSyntacticUseRestrictions(const Expr *E, const DeclContext *DC,
14551460
}
14561461
}
14571462
}
1463+
1464+
void checkClosure(ClosureExpr *closure) {
1465+
if (closure->isImplicit()) {
1466+
return;
1467+
}
1468+
1469+
auto attr = closure->getAttrs().getAttribute<ConcurrentAttr>();
1470+
if (!attr) {
1471+
return;
1472+
}
1473+
1474+
if (closure->getAsyncLoc().isValid()) {
1475+
return;
1476+
}
1477+
1478+
if (closure->getType()->castTo<AnyFunctionType>()->isAsync()) {
1479+
return;
1480+
}
1481+
1482+
// `@concurrent` is not allowed on synchronous closures, but this is
1483+
// likely to change, so diagnose this here, post solution application,
1484+
// instead of introducing an "implies `async`" inference rule that won't
1485+
// make sense in the nearish future.
1486+
Ctx.Diags.diagnose(attr->getLocation(),
1487+
diag::execution_behavior_only_on_async_closure, attr);
1488+
attr->setInvalid();
1489+
}
14581490
};
14591491

14601492
DiagnoseWalker Walker(DC, isExprStmt);

lib/Sema/TypeCheckAttr.cpp

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8297,16 +8297,6 @@ class ClosureAttributeChecker
82978297
}
82988298

82998299
void checkExecutionBehaviorAttribute(DeclAttribute *attr) {
8300-
// execution behavior attribute implies `async`.
8301-
if (closure->hasExplicitResultType() &&
8302-
closure->getAsyncLoc().isInvalid()) {
8303-
ctx.Diags
8304-
.diagnose(attr->getLocation(),
8305-
diag::execution_behavior_only_on_async_closure, attr)
8306-
.fixItRemove(attr->getRangeWithAt());
8307-
attr->setInvalid();
8308-
}
8309-
83108300
if (auto actorType = getExplicitGlobalActor(closure)) {
83118301
ctx.Diags
83128302
.diagnose(

test/Concurrency/attr_execution/conversions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ do {
9393

9494
do {
9595
let _: () -> Void = { @concurrent in
96-
// expected-error@-1 {{invalid conversion from 'async' function of type '() async -> Void' to synchronous function type '() -> Void'}}
96+
// expected-error@-1 {{cannot use @concurrent on non-async closure}}{{none}}
9797
}
9898
}
9999

test/attr/execution_behavior_attrs.swift

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,74 @@ struct IsolatedType {
125125
@concurrent func test() async {}
126126
}
127127

128-
_ = { @concurrent in // Ok
128+
// `@concurrent` on closures does not contribute to `async` inference.
129+
do {
130+
_ = { @concurrent in }
131+
// expected-error@-1:9 {{cannot use @concurrent on non-async closure}}{{none}}
132+
_ = { @concurrent () -> Int in }
133+
// expected-error@-1:9 {{cannot use @concurrent on non-async closure}}{{none}}
134+
// Explicit effect precludes effects inference from the body.
135+
_ = { @concurrent () throws in await awaitMe() }
136+
// expected-error@-1:9 {{cannot use @concurrent on non-async closure}}{{none}}
137+
// expected-error@-2:40 {{'async' call in a function that does not support concurrency}}{{none}}
138+
_ = { @concurrent () async in }
139+
_ = { @concurrent in await awaitMe() }
140+
141+
func awaitMe() async {}
142+
143+
do {
144+
func foo(_: () -> Void) {}
145+
func foo(_: () async -> Void) async {}
146+
147+
func sync() {
148+
foo { @concurrent in }
149+
// expected-error@-1:13 {{cannot use @concurrent on non-async closure}}{{none}}
150+
}
151+
// expected-note@+1:10 {{add 'async' to function 'sync2()' to make it asynchronous}}{{17-17= async}}
152+
func sync2() {
153+
foo { @concurrent in await awaitMe() }
154+
// expected-error@-1:7 {{'async' call in a function that does not support concurrency}}{{none}}
155+
}
156+
func async() async {
157+
// Even though the context is async, the sync overload is favored because
158+
// the closure is sync, so the error is expected.
159+
foo { @concurrent in }
160+
// expected-error@-1:13 {{cannot use @concurrent on non-async closure}}{{none}}
161+
162+
foo { @concurrent in await awaitMe() }
163+
// expected-error@-1:7 {{expression is 'async' but is not marked with 'await'}}{{7-7=await }}
164+
// expected-note@-2:7 {{call is 'async'}}{{none}}
165+
}
166+
}
167+
168+
do {
169+
func foo(_: (Int) async -> Void) {}
170+
func foo(_: (Int) -> Void) async {}
171+
172+
func sync() {
173+
// OK, sync overload picked.
174+
foo { @concurrent _ in }
175+
}
176+
func async() async {
177+
foo { @concurrent _ in }
178+
// expected-error@-1:13 {{cannot use @concurrent on non-async closure}}{{none}}
179+
// expected-error@-2:7 {{expression is 'async' but is not marked with 'await'}}{{7-7=await }}
180+
// expected-note@-3:7 {{call is 'async'}}{{none}}
181+
}
182+
}
183+
184+
do {
185+
func foo<T>(_: T) {}
186+
187+
foo({@concurrent in })
188+
// expected-error@-1:10 {{cannot use @concurrent on non-async closure}}{{none}}
189+
}
129190
}
130191

131192
_ = { @MainActor @concurrent in
132193
// expected-error@-1 {{cannot use @concurrent because function type is isolated to a global actor 'MainActor'}}
133194
}
134195

135-
_ = { @concurrent () -> Int in
136-
// expected-error@-1 {{@concurrent on non-async closure}}
137-
}
138-
139196
// Make sure that explicit use of `@concurrent` doesn't interfere with inference of `throws` from the body.
140197
do {
141198
func acceptsThrowing(_ body: () async throws -> Void) async {

0 commit comments

Comments
 (0)