Skip to content

Commit f36f014

Browse files
authored
Merge pull request #34551 from kavon/async-rethrows-70751405
[concurrency] type-checking call-sites that involve an async throws / rethrows callee
2 parents 12d1147 + d4f4dd3 commit f36f014

File tree

3 files changed

+196
-9
lines changed

3 files changed

+196
-9
lines changed

lib/Sema/TypeCheckEffects.cpp

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,17 @@ class PotentialThrowReason {
272272
CallRethrowsWithDefaultThrowingArgument,
273273
};
274274

275+
static StringRef kindToString(Kind k) {
276+
switch (k) {
277+
case Kind::Throw: return "Throw";
278+
case Kind::CallThrows: return "CallThrows";
279+
case Kind::CallRethrowsWithExplicitThrowingArgument:
280+
return "CallRethrowsWithExplicitThrowingArgument";
281+
case Kind::CallRethrowsWithDefaultThrowingArgument:
282+
return "CallRethrowsWithDefaultThrowingArgument";
283+
}
284+
}
285+
275286
private:
276287
Expr *TheExpression;
277288
Kind TheKind;
@@ -329,6 +340,26 @@ class Classification {
329340
ThrowingKind Result = ThrowingKind::None;
330341
Optional<PotentialThrowReason> Reason;
331342

343+
void print(raw_ostream &out) const {
344+
out << "{ IsInvalid = " << IsInvalid
345+
<< ", IsAsync = " << IsAsync
346+
<< ", Result = ThrowingKind::";
347+
348+
switch(Result) {
349+
case ThrowingKind::None: out << "None"; break;
350+
case ThrowingKind::RethrowingOnly: out << "RethrowingOnly"; break;
351+
case ThrowingKind::Throws: out << "Throws"; break;
352+
}
353+
354+
out << ", Reason = ";
355+
if (!Reason)
356+
out << "nil";
357+
else
358+
out << PotentialThrowReason::kindToString(Reason.getValue().getKind());
359+
360+
out << " }";
361+
}
362+
332363
public:
333364
Classification() : Result(ThrowingKind::None) {}
334365
explicit Classification(ThrowingKind result, PotentialThrowReason reason,
@@ -364,17 +395,21 @@ class Classification {
364395
return result;
365396
}
366397

367-
static Classification forRethrowingOnly(PotentialThrowReason reason) {
398+
static Classification forRethrowingOnly(PotentialThrowReason reason, bool isAsync) {
368399
Classification result;
369400
result.Result = ThrowingKind::RethrowingOnly;
370401
result.Reason = reason;
402+
result.IsAsync = isAsync;
371403
return result;
372404
}
373405

374406
void merge(Classification other) {
407+
bool oldAsync = IsAsync;
408+
375409
if (other.getResult() > getResult())
376410
*this = other;
377-
IsAsync |= other.IsAsync;
411+
412+
IsAsync = oldAsync | other.IsAsync;
378413
}
379414

380415
bool isInvalid() const { return IsInvalid; }
@@ -386,6 +421,10 @@ class Classification {
386421
}
387422

388423
bool isAsync() const { return IsAsync; }
424+
425+
#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
426+
LLVM_DUMP_METHOD void dump() const { print(llvm::errs()); }
427+
#endif
389428
};
390429

391430

@@ -399,7 +438,7 @@ class ApplyClassifier {
399438
DeclContext *RethrowsDC = nullptr;
400439
bool inRethrowsContext() const { return RethrowsDC != nullptr; }
401440

402-
/// Check to see if the given function application throws.
441+
/// Check to see if the given function application throws or is async.
403442
Classification classifyApply(ApplyExpr *E) {
404443
// An apply expression is a potential throw site if the function throws.
405444
// But if the expression didn't type-check, suppress diagnostics.
@@ -461,7 +500,8 @@ class ApplyClassifier {
461500
if (!type) return Classification::forInvalidCode();
462501

463502
// Use the most significant result from the arguments.
464-
Classification result;
503+
Classification result = isAsync ? Classification::forAsync()
504+
: Classification();
465505
for (auto arg : llvm::reverse(args)) {
466506
auto fnType = type->getAs<AnyFunctionType>();
467507
if (!fnType) return Classification::forInvalidCode();
@@ -527,7 +567,7 @@ class ApplyClassifier {
527567
// If we're currently doing rethrows-checking on the body of the
528568
// function which declares the parameter, it's rethrowing-only.
529569
if (param->getDeclContext() == RethrowsDC)
530-
return Classification::forRethrowingOnly(reason);
570+
return Classification::forRethrowingOnly(reason, /*async*/false);
531571

532572
// Otherwise, it throws unconditionally.
533573
return Classification::forThrow(reason, /*async*/false);
@@ -1235,8 +1275,12 @@ class Context {
12351275
highlight = apply->getSourceRange();
12361276

12371277
auto diag = diag::async_call_without_await;
1238-
if (isAutoClosure())
1278+
// To produce a better error message, check if it is an autoclosure.
1279+
// We do not use 'Context::isAutoClosure' b/c it gives conservative answers.
1280+
if (Function && llvm::isa_and_nonnull<AutoClosureExpr>(
1281+
Function->getAbstractClosureExpr()))
12391282
diag = diag::async_call_without_await_in_autoclosure;
1283+
12401284
ctx.Diags.diagnose(node.getStartLoc(), diag)
12411285
.fixItInsert(node.getStartLoc(), "await ")
12421286
.highlight(highlight);
@@ -1464,6 +1508,10 @@ class CheckEffectsCoverage : public EffectsHandlingWalker<CheckEffectsCoverage>
14641508
OldMaxThrowingKind = std::max(OldMaxThrowingKind, Self.MaxThrowingKind);
14651509
}
14661510

1511+
void preserveCoverageFromOptionalOrForcedTryOperand() {
1512+
OldFlags.mergeFrom(ContextFlags::asyncAwaitFlags(), Self.Flags);
1513+
}
1514+
14671515
void preserveCoverageFromInterpolatedString() {
14681516
OldFlags.mergeFrom(ContextFlags::HasAnyThrowSite, Self.Flags);
14691517
OldFlags.mergeFrom(ContextFlags::HasTryThrowSite, Self.Flags);
@@ -1808,6 +1856,8 @@ class CheckEffectsCoverage : public EffectsHandlingWalker<CheckEffectsCoverage>
18081856
if (!Flags.has(ContextFlags::HasTryThrowSite)) {
18091857
Ctx.Diags.diagnose(E->getLoc(), diag::no_throw_in_try);
18101858
}
1859+
1860+
scope.preserveCoverageFromOptionalOrForcedTryOperand();
18111861
return ShouldNotRecurse;
18121862
}
18131863

@@ -1822,6 +1872,8 @@ class CheckEffectsCoverage : public EffectsHandlingWalker<CheckEffectsCoverage>
18221872
if (!Flags.has(ContextFlags::HasTryThrowSite)) {
18231873
Ctx.Diags.diagnose(E->getLoc(), diag::no_throw_in_try);
18241874
}
1875+
1876+
scope.preserveCoverageFromOptionalOrForcedTryOperand();
18251877
return ShouldNotRecurse;
18261878
}
18271879
};

test/Concurrency/async_cancellation.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,13 @@ func test_cancellation_loop() async -> Int {
5353
func int() async -> Int { 42 }
5454

5555
func test_cancellation_withDeadline_in() async throws -> Int {
56-
/* missing await */ Task.withDeadline(in: .seconds(5), operation: { // FIXME: rdar://70751405 async rethrows functions are not detected as async
56+
await Task.withDeadline(in: .seconds(5), operation: {
5757
await int()
5858
})
5959
}
6060

6161
func test_cancellation_withDeadline(specificDeadline: Task.Deadline) async -> Int {
62-
/* missing `await` */
63-
Task.withDeadline(specificDeadline) { // FIXME: rdar://70751405 async rethrows functions are not detected as async
62+
await Task.withDeadline(specificDeadline) {
6463
await int()
6564
}
6665
}

test/Concurrency/async_throwing.swift

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// RUN: %target-typecheck-verify-swift -enable-experimental-concurrency
2+
// REQUIRES: concurrency
3+
4+
// These tests cover various interactions with async functions that are
5+
// either throws or rethrows.
6+
// See rdar://70813762 and rdar://70751405
7+
8+
enum InvocationError : Error {
9+
case ErrVal
10+
}
11+
12+
func asyncThrows() async throws {
13+
throw InvocationError.ErrVal
14+
}
15+
16+
// T = Int
17+
func asyncRethrows(fn : () async throws -> Int) async rethrows -> Int {
18+
return await try fn()
19+
}
20+
21+
// T = String
22+
func asyncRethrows(fn : () async throws -> String) async rethrows -> String {
23+
return await try fn()
24+
}
25+
26+
// Generic. NOTE the 'rethrows'
27+
func invoke<T>(fn : () async throws -> T) async rethrows -> T {
28+
return await try fn()
29+
}
30+
31+
// NOTE the 'rethrows'
32+
func invokeAuto<T>(_ val : @autoclosure () async throws -> T) async rethrows -> T {
33+
return await try val()
34+
}
35+
36+
func normalTask() async -> Int {
37+
return 42
38+
}
39+
40+
func throwingTask() async throws -> String {
41+
if 1.0 / 3.0 == 0.33 {
42+
throw InvocationError.ErrVal
43+
}
44+
return "ok!"
45+
}
46+
47+
// expected-note@+2 7 {{add '@asyncHandler' to function 'syncTest()' to create an implicit asynchronous context}}
48+
// expected-note@+1 7 {{add 'async' to function 'syncTest()' to make it asynchronous}}
49+
func syncTest() {
50+
let _ = invoke(fn: normalTask) // expected-error{{'async' in a function that does not support concurrency}}
51+
let _ = invokeAuto(42) // expected-error{{'async' in a function that does not support concurrency}}
52+
let _ = invokeAuto("intuitive") // expected-error{{'async' in a function that does not support concurrency}}
53+
54+
let _ = try! asyncRethrows(fn: throwingTask) // expected-error{{'async' in a function that does not support concurrency}}
55+
let _ = try? invoke(fn: throwingTask) // expected-error{{'async' in a function that does not support concurrency}}
56+
do {
57+
let _ = try invoke(fn: throwingTask) // expected-error{{'async' in a function that does not support concurrency}}
58+
let _ = try asyncThrows() // expected-error{{'async' in a function that does not support concurrency}}
59+
} catch {
60+
// ignore it
61+
}
62+
}
63+
64+
65+
func asyncTest() async {
66+
///////////
67+
// tests that also omit await
68+
69+
let _ = invoke(fn: normalTask) // expected-error{{call is 'async' but is not marked with 'await'}}
70+
let _ = asyncRethrows(fn: normalTask) // expected-error{{call is 'async' but is not marked with 'await'}}
71+
let _ = invokeAuto(42) // expected-error{{call is 'async' but is not marked with 'await'}}
72+
73+
// expected-error@+2 {{call can throw, but it is not marked with 'try' and the error is not handled}}
74+
// expected-error@+1 {{call is 'async' but is not marked with 'await'}}
75+
let _ = asyncThrows()
76+
77+
// expected-note@+3{{call is to 'rethrows' function, but argument function can throw}}
78+
// expected-error@+2{{call can throw, but it is not marked with 'try' and the error is not handled}}
79+
// expected-error@+1{{call is 'async' but is not marked with 'await'}}
80+
let _ = invoke(fn: throwingTask)
81+
82+
///////////
83+
// tests that use await and handles the exceptions
84+
85+
// expected-note@+2{{call is to 'rethrows' function, but argument function can throw}}
86+
// expected-error@+1{{call can throw, but it is not marked with 'try' and the error is not handled}}
87+
let _ = await invoke(fn: throwingTask)
88+
let _ = await invoke(fn: normalTask) // ok
89+
90+
let _ = await asyncRethrows(fn: normalTask)
91+
let _ = await try! asyncRethrows(fn: normalTask) // expected-warning{{no calls to throwing functions occur within 'try' expression}}
92+
let _ = await try? asyncRethrows(fn: normalTask) // expected-warning{{no calls to throwing functions occur within 'try' expression}}
93+
94+
let _ = await try! asyncRethrows(fn: throwingTask)
95+
let _ = await try? asyncRethrows(fn: throwingTask)
96+
let _ = await try! asyncThrows()
97+
let _ = await try? asyncThrows()
98+
99+
//////////
100+
// some auto-closure tests
101+
102+
let _ = await invokeAuto("intuitive")
103+
let _ = await try! invokeAuto(await throwingTask())
104+
let _ = await try? invokeAuto(await throwingTask())
105+
let _ = await invokeAuto(await try! throwingTask())
106+
let _ = await invokeAuto(await try? throwingTask())
107+
108+
let _ = await invokeAuto(try! throwingTask()) // expected-error{{call is 'async' in an autoclosure argument that is not marked with 'await'}}
109+
let _ = await invokeAuto(try? throwingTask()) // expected-error{{call is 'async' in an autoclosure argument that is not marked with 'await'}}
110+
111+
let _ = await invokeAuto(await try! throwingTask())
112+
let _ = await invokeAuto(await try? throwingTask())
113+
/////////
114+
115+
do {
116+
let _ = await try asyncThrows()
117+
let _ = await try asyncRethrows(fn: throwingTask)
118+
119+
//////
120+
// more auto-closure tests
121+
122+
// expected-note@+6 {{did you mean to disable error propagation?}}
123+
// expected-note@+5 {{did you mean to handle error as optional value?}}
124+
// expected-note@+4 {{did you mean to use 'try'?}}
125+
// expected-note@+3 {{call is to 'rethrows' function, but argument function can throw}}
126+
// expected-error@+2 {{call is 'async' in an autoclosure argument that is not marked with 'await'}}
127+
// expected-error@+1 2 {{call can throw but is not marked with 'try'}}
128+
let _ = await invokeAuto(throwingTask())
129+
130+
let _ = await try invokeAuto(throwingTask()) // expected-error{{call is 'async' in an autoclosure argument that is not marked with 'await'}}
131+
let _ = try invokeAuto(await throwingTask()) // expected-error{{call is 'async' but is not marked with 'await'}}
132+
let _ = await try invokeAuto(await throwingTask())
133+
} catch {
134+
// ignore
135+
}
136+
}

0 commit comments

Comments
 (0)