Skip to content

Commit 2d4e8fd

Browse files
committed
[Typed throws] Compute and use the caught error type of a do..catch block.
The type that is caught by the `catch` clauses in a `do..catch` block is determined by the union of the thrown error types in the `do` statement. Compute this type and use it for the catch clauses. This does several things at once: * Makes the type of the implicit `error` be a more-specific concrete type when all throwing sites throw that same type * When there's a concrete type for the error, one can use patterns like `.cancelled` * Check that this error type can be rethrown in the current context * Verify that SIL generation involving do..catch with typed errors doesn't require any existentials.
1 parent 69ec45d commit 2d4e8fd

File tree

8 files changed

+239
-8
lines changed

8 files changed

+239
-8
lines changed

include/swift/AST/Stmt.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,12 @@ class DoCatchStmt final
14251425
/// errors out of its catch block(s).
14261426
bool isSyntacticallyExhaustive() const;
14271427

1428+
// Determines the type of the error that is thrown out of the 'do' block
1429+
// and caught by the various 'catch' clauses. If this the catch clauses
1430+
// aren't exhausive, this is also the type of the error that is implicitly
1431+
// rethrown.
1432+
Type getCaughtErrorType() const;
1433+
14281434
static bool classof(const Stmt *S) {
14291435
return S->getKind() == StmtKind::DoCatch;
14301436
}

lib/AST/Stmt.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,15 @@ bool DoCatchStmt::isSyntacticallyExhaustive() const {
476476
return false;
477477
}
478478

479+
Type DoCatchStmt::getCaughtErrorType() const {
480+
return getCatches()
481+
.front()
482+
->getCaseLabelItems()
483+
.front()
484+
.getPattern()
485+
->getType();
486+
}
487+
479488
void LabeledConditionalStmt::setCond(StmtCondition e) {
480489
// When set a condition into a Conditional Statement, inform each of the
481490
// variables bound in any patterns that this is the owning statement for the

lib/SILGen/SILGenStmt.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,12 +1111,7 @@ void StmtEmitter::visitDoStmt(DoStmt *S) {
11111111
}
11121112

11131113
void StmtEmitter::visitDoCatchStmt(DoCatchStmt *S) {
1114-
Type formalExnType = S->getCatches()
1115-
.front()
1116-
->getCaseLabelItems()
1117-
.front()
1118-
.getPattern()
1119-
->getType();
1114+
Type formalExnType = S->getCaughtErrorType();
11201115
auto &exnTL = SGF.getTypeLowering(formalExnType);
11211116

11221117
// Create the throw destination at the end of the function.

lib/Sema/TypeCheckEffects.cpp

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,11 @@ class Classification {
864864
}
865865
llvm_unreachable("Bad effect kind");
866866
}
867+
Type getThrownError() const {
868+
assert(ThrowKind == ConditionalEffectKind::Always ||
869+
ThrowKind == ConditionalEffectKind::Conditional);
870+
return ThrownError;
871+
}
867872
PotentialEffectReason getThrowReason() const {
868873
assert(ThrowKind == ConditionalEffectKind::Always ||
869874
ThrowKind == ConditionalEffectKind::Conditional);
@@ -1131,7 +1136,7 @@ class ApplyClassifier {
11311136
case EffectKind::Throws: {
11321137
FunctionThrowsClassifier classifier(*this);
11331138
expr->walk(classifier);
1134-
return classifier.classification;
1139+
return classifier.classification.onlyThrowing();
11351140
}
11361141
case EffectKind::Async: {
11371142
FunctionAsyncClassifier classifier(*this);
@@ -1143,6 +1148,23 @@ class ApplyClassifier {
11431148
llvm_unreachable("Bad effect");
11441149
}
11451150

1151+
// Classify a single statement without considering its enclosing context.
1152+
Classification classifyStmt(Stmt *stmt, EffectKind kind) {
1153+
switch (kind) {
1154+
case EffectKind::Throws: {
1155+
FunctionThrowsClassifier classifier(*this);
1156+
stmt->walk(classifier);
1157+
return classifier.classification.onlyThrowing();
1158+
}
1159+
case EffectKind::Async: {
1160+
FunctionAsyncClassifier classifier(*this);
1161+
stmt->walk(classifier);
1162+
return Classification::forAsync(
1163+
classifier.AsyncKind, /*FIXME:*/PotentialEffectReason::forApply());
1164+
}
1165+
}
1166+
}
1167+
11461168
private:
11471169
/// Classify a throwing or async function according to our local
11481170
/// knowledge of its implementation.
@@ -3242,6 +3264,26 @@ bool TypeChecker::canThrow(ASTContext &ctx, Expr *expr) {
32423264
ConditionalEffectKind::None;
32433265
}
32443266

3267+
Type TypeChecker::catchErrorType(ASTContext &ctx, DoCatchStmt *stmt) {
3268+
// When typed throws is disabled, this is always "any Error".
3269+
// FIXME: When we distinguish "precise" typed throws from normal typed
3270+
// throws, we'll be able to compute a more narrow catch error type in some
3271+
// case, e.g., from a `try` but not a `throws`.
3272+
if (!ctx.LangOpts.hasFeature(Feature::TypedThrows))
3273+
return ctx.getErrorExistentialType();
3274+
3275+
// Classify the throwing behavior of the "do" body.
3276+
ApplyClassifier classifier(ctx);
3277+
Classification classification = classifier.classifyStmt(
3278+
stmt->getBody(), EffectKind::Throws);
3279+
3280+
// If it doesn't throw at all, the type is Never.
3281+
if (!classification.hasThrows())
3282+
return ctx.getNeverType();
3283+
3284+
return classification.getThrownError();
3285+
}
3286+
32453287
Type TypeChecker::errorUnion(Type type1, Type type2) {
32463288
// If one type is NULL, return the other.
32473289
if (!type1)

lib/Sema/TypeCheckStmt.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1678,10 +1678,26 @@ class StmtChecker : public StmtVisitor<StmtChecker, Stmt*> {
16781678
// Do-catch statements always limit exhaustivity checks.
16791679
bool limitExhaustivityChecks = true;
16801680

1681+
Type caughtErrorType = TypeChecker::catchErrorType(Ctx, S);
16811682
auto catches = S->getCatches();
16821683
checkSiblingCaseStmts(catches.begin(), catches.end(),
16831684
CaseParentKind::DoCatch, limitExhaustivityChecks,
1684-
getASTContext().getErrorExistentialType());
1685+
caughtErrorType);
1686+
1687+
if (!S->isSyntacticallyExhaustive()) {
1688+
// If we're implicitly rethrowing the error out of this do..catch, make
1689+
// sure that we can throw an error of this type out of this context.
1690+
// FIXME: Unify this lookup of the type with that from ThrowStmt.
1691+
if (auto TheFunc = AnyFunctionRef::fromDeclContext(DC)) {
1692+
if (Type expectedErrorType = TheFunc->getThrownErrorType()) {
1693+
OpaqueValueExpr *opaque = new (Ctx) OpaqueValueExpr(
1694+
catches.back()->getEndLoc(), caughtErrorType);
1695+
Expr *rethrowExpr = opaque;
1696+
TypeChecker::typeCheckExpression(
1697+
rethrowExpr, DC, {expectedErrorType, CTP_ThrowStmt});
1698+
}
1699+
}
1700+
}
16851701

16861702
return S;
16871703
}

lib/Sema/TypeChecker.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,13 @@ void checkPropertyWrapperEffects(PatternBindingDecl *binding, Expr *expr);
11781178
/// Whether the given expression can throw.
11791179
bool canThrow(ASTContext &ctx, Expr *expr);
11801180

1181+
/// Determine the error type that is thrown out of the body of the given
1182+
/// do-catch statement.
1183+
///
1184+
/// The error type is used in the catch clauses and, for a nonexhausive
1185+
/// do-catch, is implicitly rethrown out of the do...catch block.
1186+
Type catchErrorType(ASTContext &ctx, DoCatchStmt *stmt);
1187+
11811188
/// Given two error types, merge them into the "union" of both error types
11821189
/// that is a supertype of both error types.
11831190
Type errorUnion(Type type1, Type type2);

test/SILGen/typed_throws.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
enum MyError: Error {
44
case fail
5+
case epicFail
56
}
67

78
enum MyBigError: Error {
@@ -80,3 +81,37 @@ func throwsOneOrTheOtherWithRethrow() throws {
8081
sink(be)
8182
}
8283
}
84+
85+
// CHECK-LABEL: sil hidden [ossa] @$s12typed_throws0B26ConcreteWithDoCatchRethrowyyKF : $@convention(thin) () -> @error any Error
86+
func throwsConcreteWithDoCatchRethrow() throws {
87+
do {
88+
// CHECK: [[FN:%[0-9]+]] = function_ref @$s12typed_throws0B8ConcreteyyKF : $@convention(thin) () -> @error MyError
89+
// CHECK: try_apply [[FN]]() : $@convention(thin) () -> @error MyError, normal [[NORMAL_BB:bb[0-9]+]], error [[ERROR_BB:bb[0-9]+]]
90+
try throwsConcrete()
91+
92+
// CHECK: [[ERROR_BB]]([[ERROR:%[0-9]+]] : $MyError):
93+
// CHECK-NEXT: switch_enum [[ERROR]] : $MyError, case #MyError.fail!enumelt: [[FAILCASE_BB:bb[0-9]+]], default [[DEFAULT_BB:bb[0-9]+]]
94+
} catch .fail {
95+
}
96+
97+
// CHECK: [[DEFAULT_BB]]:
98+
// CHECK-NOT: throw
99+
// CHECK: alloc_existential_box $any Error
100+
// CHECK: throw [[ERR:%[0-9]+]] : $any Error
101+
}
102+
103+
// CHECK-LABEL: sil hidden [ossa] @$s12typed_throws0B31ConcreteWithDoCatchTypedRethrowyyKF : $@convention(thin) () -> @error MyError
104+
func throwsConcreteWithDoCatchTypedRethrow() throws(MyError) {
105+
do {
106+
// CHECK: [[FN:%[0-9]+]] = function_ref @$s12typed_throws0B8ConcreteyyKF : $@convention(thin) () -> @error MyError
107+
// CHECK: try_apply [[FN]]() : $@convention(thin) () -> @error MyError, normal [[NORMAL_BB:bb[0-9]+]], error [[ERROR_BB:bb[0-9]+]]
108+
try throwsConcrete()
109+
110+
// CHECK: [[ERROR_BB]]([[ERROR:%[0-9]+]] : $MyError):
111+
// CHECK-NEXT: switch_enum [[ERROR]] : $MyError, case #MyError.fail!enumelt: [[FAILCASE_BB:bb[0-9]+]], default [[DEFAULT_BB:bb[0-9]+]]
112+
} catch .fail {
113+
}
114+
115+
// CHECK: [[DEFAULT_BB]]:
116+
// CHECK-NEXT: throw [[ERROR]] : $MyError
117+
}

test/stmt/typed_throws.swift

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// RUN: %target-typecheck-verify-swift -enable-experimental-feature TypedThrows
2+
3+
enum MyError: Error {
4+
case failed
5+
case epicFailed
6+
}
7+
8+
enum HomeworkError: Error {
9+
case dogAteIt
10+
case forgot
11+
}
12+
13+
func processMyError(_: MyError) { }
14+
15+
func doSomething() throws(MyError) { }
16+
func doHomework() throws(HomeworkError) { }
17+
18+
func testDoCatchErrorTyped() {
19+
#if false
20+
// FIXME: Deal with throws directly in the do...catch blocks.
21+
do {
22+
throw MyError.failed
23+
} catch {
24+
assert(error == .failed)
25+
processMyError(error)
26+
}
27+
#endif
28+
29+
// Throwing a typed error in a do...catch catches the error with that type.
30+
do {
31+
try doSomething()
32+
} catch {
33+
assert(error == .failed)
34+
processMyError(error)
35+
}
36+
37+
// Throwing a typed error in a do...catch lets us pattern-match against that
38+
// type.
39+
do {
40+
try doSomething()
41+
} catch .failed {
42+
// okay, matches one of the cases of MyError
43+
} catch {
44+
assert(error == .epicFailed)
45+
}
46+
47+
// Rethrowing an error because the catch is not exhaustive.
48+
do {
49+
try doSomething()
50+
// expected-error@-1{{errors thrown from here are not handled because the enclosing catch is not exhaustive}}
51+
} catch .failed {
52+
}
53+
54+
// "as X" errors are never exhaustive.
55+
do {
56+
try doSomething()
57+
// FIXME: should error errors thrown from here are not handled because the enclosing catch is not exhaustive
58+
} catch let error as MyError { // expected-warning{{'as' test is always true}}
59+
_ = error
60+
}
61+
62+
// Rethrowing an error because the catch is not exhaustive.
63+
do {
64+
try doSomething()
65+
// expected-error@-1{{errors thrown from here are not handled because the enclosing catch is not exhaustive}}
66+
} catch is HomeworkError {
67+
// expected-warning@-1{{cast from 'MyError' to unrelated type 'HomeworkError' always fails}}
68+
}
69+
}
70+
71+
func testDoCatchMultiErrorType() {
72+
// Throwing different typed errors results in 'any Error'
73+
do {
74+
try doSomething()
75+
try doHomework()
76+
} catch .failed { // expected-error{{type 'any Error' has no member 'failed'}}
77+
78+
} catch {
79+
let _: Int = error // expected-error{{cannot convert value of type 'any Error' to specified type 'Int'}}
80+
}
81+
}
82+
83+
func testDoCatchRethrowsUntyped() throws {
84+
do {
85+
try doSomething()
86+
} catch .failed {
87+
} // okay, rethrows with a conversion to 'any Error'
88+
}
89+
90+
func testDoCatchRethrowsTyped() throws(HomeworkError) {
91+
do {
92+
try doHomework()
93+
} catch .dogAteIt {
94+
} // okay, rethrows
95+
96+
do {
97+
try doSomething()
98+
} catch .failed {
99+
100+
} // expected-error{{thrown expression type 'MyError' cannot be converted to error type 'HomeworkError'}}
101+
102+
do {
103+
try doSomething()
104+
try doHomework()
105+
} catch let e as HomeworkError {
106+
_ = e
107+
} // expected-error{{thrown expression type 'any Error' cannot be converted to error type 'HomeworkError'}}
108+
109+
do {
110+
try doSomething()
111+
try doHomework()
112+
} catch {
113+
114+
} // okay, the thrown 'any Error' has been caught
115+
}
116+
117+
func testTryIncompatibleTyped() throws(HomeworkError) {
118+
try doHomework() // okay
119+
120+
try doSomething() // FIXME: should error
121+
}

0 commit comments

Comments
 (0)