From 2d4e8fda3ec5c85ca37066cfcfc225743cc9390f Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 4 Oct 2023 17:02:02 -0700 Subject: [PATCH] [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. --- include/swift/AST/Stmt.h | 6 ++ lib/AST/Stmt.cpp | 9 +++ lib/SILGen/SILGenStmt.cpp | 7 +- lib/Sema/TypeCheckEffects.cpp | 44 +++++++++++- lib/Sema/TypeCheckStmt.cpp | 18 ++++- lib/Sema/TypeChecker.h | 7 ++ test/SILGen/typed_throws.swift | 35 ++++++++++ test/stmt/typed_throws.swift | 121 +++++++++++++++++++++++++++++++++ 8 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 test/stmt/typed_throws.swift diff --git a/include/swift/AST/Stmt.h b/include/swift/AST/Stmt.h index f62ff4bf34313..c35722d66cbc9 100644 --- a/include/swift/AST/Stmt.h +++ b/include/swift/AST/Stmt.h @@ -1425,6 +1425,12 @@ class DoCatchStmt final /// errors out of its catch block(s). bool isSyntacticallyExhaustive() const; + // Determines the type of the error that is thrown out of the 'do' block + // and caught by the various 'catch' clauses. If this the catch clauses + // aren't exhausive, this is also the type of the error that is implicitly + // rethrown. + Type getCaughtErrorType() const; + static bool classof(const Stmt *S) { return S->getKind() == StmtKind::DoCatch; } diff --git a/lib/AST/Stmt.cpp b/lib/AST/Stmt.cpp index 0966075df8bd1..e4ce009f5f7ba 100644 --- a/lib/AST/Stmt.cpp +++ b/lib/AST/Stmt.cpp @@ -476,6 +476,15 @@ bool DoCatchStmt::isSyntacticallyExhaustive() const { return false; } +Type DoCatchStmt::getCaughtErrorType() const { + return getCatches() + .front() + ->getCaseLabelItems() + .front() + .getPattern() + ->getType(); +} + void LabeledConditionalStmt::setCond(StmtCondition e) { // When set a condition into a Conditional Statement, inform each of the // variables bound in any patterns that this is the owning statement for the diff --git a/lib/SILGen/SILGenStmt.cpp b/lib/SILGen/SILGenStmt.cpp index ee041ccaf18cc..6c3aa6426ad04 100644 --- a/lib/SILGen/SILGenStmt.cpp +++ b/lib/SILGen/SILGenStmt.cpp @@ -1111,12 +1111,7 @@ void StmtEmitter::visitDoStmt(DoStmt *S) { } void StmtEmitter::visitDoCatchStmt(DoCatchStmt *S) { - Type formalExnType = S->getCatches() - .front() - ->getCaseLabelItems() - .front() - .getPattern() - ->getType(); + Type formalExnType = S->getCaughtErrorType(); auto &exnTL = SGF.getTypeLowering(formalExnType); // Create the throw destination at the end of the function. diff --git a/lib/Sema/TypeCheckEffects.cpp b/lib/Sema/TypeCheckEffects.cpp index b2d031029908e..39bb3495988a1 100644 --- a/lib/Sema/TypeCheckEffects.cpp +++ b/lib/Sema/TypeCheckEffects.cpp @@ -864,6 +864,11 @@ class Classification { } llvm_unreachable("Bad effect kind"); } + Type getThrownError() const { + assert(ThrowKind == ConditionalEffectKind::Always || + ThrowKind == ConditionalEffectKind::Conditional); + return ThrownError; + } PotentialEffectReason getThrowReason() const { assert(ThrowKind == ConditionalEffectKind::Always || ThrowKind == ConditionalEffectKind::Conditional); @@ -1131,7 +1136,7 @@ class ApplyClassifier { case EffectKind::Throws: { FunctionThrowsClassifier classifier(*this); expr->walk(classifier); - return classifier.classification; + return classifier.classification.onlyThrowing(); } case EffectKind::Async: { FunctionAsyncClassifier classifier(*this); @@ -1143,6 +1148,23 @@ class ApplyClassifier { llvm_unreachable("Bad effect"); } + // Classify a single statement without considering its enclosing context. + Classification classifyStmt(Stmt *stmt, EffectKind kind) { + switch (kind) { + case EffectKind::Throws: { + FunctionThrowsClassifier classifier(*this); + stmt->walk(classifier); + return classifier.classification.onlyThrowing(); + } + case EffectKind::Async: { + FunctionAsyncClassifier classifier(*this); + stmt->walk(classifier); + return Classification::forAsync( + classifier.AsyncKind, /*FIXME:*/PotentialEffectReason::forApply()); + } + } + } + private: /// Classify a throwing or async function according to our local /// knowledge of its implementation. @@ -3242,6 +3264,26 @@ bool TypeChecker::canThrow(ASTContext &ctx, Expr *expr) { ConditionalEffectKind::None; } +Type TypeChecker::catchErrorType(ASTContext &ctx, DoCatchStmt *stmt) { + // When typed throws is disabled, this is always "any Error". + // FIXME: When we distinguish "precise" typed throws from normal typed + // throws, we'll be able to compute a more narrow catch error type in some + // case, e.g., from a `try` but not a `throws`. + if (!ctx.LangOpts.hasFeature(Feature::TypedThrows)) + return ctx.getErrorExistentialType(); + + // Classify the throwing behavior of the "do" body. + ApplyClassifier classifier(ctx); + Classification classification = classifier.classifyStmt( + stmt->getBody(), EffectKind::Throws); + + // If it doesn't throw at all, the type is Never. + if (!classification.hasThrows()) + return ctx.getNeverType(); + + return classification.getThrownError(); +} + Type TypeChecker::errorUnion(Type type1, Type type2) { // If one type is NULL, return the other. if (!type1) diff --git a/lib/Sema/TypeCheckStmt.cpp b/lib/Sema/TypeCheckStmt.cpp index d9965d1665e94..12f6a708fb671 100644 --- a/lib/Sema/TypeCheckStmt.cpp +++ b/lib/Sema/TypeCheckStmt.cpp @@ -1678,10 +1678,26 @@ class StmtChecker : public StmtVisitor { // Do-catch statements always limit exhaustivity checks. bool limitExhaustivityChecks = true; + Type caughtErrorType = TypeChecker::catchErrorType(Ctx, S); auto catches = S->getCatches(); checkSiblingCaseStmts(catches.begin(), catches.end(), CaseParentKind::DoCatch, limitExhaustivityChecks, - getASTContext().getErrorExistentialType()); + caughtErrorType); + + if (!S->isSyntacticallyExhaustive()) { + // If we're implicitly rethrowing the error out of this do..catch, make + // sure that we can throw an error of this type out of this context. + // FIXME: Unify this lookup of the type with that from ThrowStmt. + if (auto TheFunc = AnyFunctionRef::fromDeclContext(DC)) { + if (Type expectedErrorType = TheFunc->getThrownErrorType()) { + OpaqueValueExpr *opaque = new (Ctx) OpaqueValueExpr( + catches.back()->getEndLoc(), caughtErrorType); + Expr *rethrowExpr = opaque; + TypeChecker::typeCheckExpression( + rethrowExpr, DC, {expectedErrorType, CTP_ThrowStmt}); + } + } + } return S; } diff --git a/lib/Sema/TypeChecker.h b/lib/Sema/TypeChecker.h index e99456c0cae4c..36dc779d9af5a 100644 --- a/lib/Sema/TypeChecker.h +++ b/lib/Sema/TypeChecker.h @@ -1178,6 +1178,13 @@ void checkPropertyWrapperEffects(PatternBindingDecl *binding, Expr *expr); /// Whether the given expression can throw. bool canThrow(ASTContext &ctx, Expr *expr); +/// Determine the error type that is thrown out of the body of the given +/// do-catch statement. +/// +/// The error type is used in the catch clauses and, for a nonexhausive +/// do-catch, is implicitly rethrown out of the do...catch block. +Type catchErrorType(ASTContext &ctx, DoCatchStmt *stmt); + /// Given two error types, merge them into the "union" of both error types /// that is a supertype of both error types. Type errorUnion(Type type1, Type type2); diff --git a/test/SILGen/typed_throws.swift b/test/SILGen/typed_throws.swift index 8df4844f05889..2b7a9b23f9741 100644 --- a/test/SILGen/typed_throws.swift +++ b/test/SILGen/typed_throws.swift @@ -2,6 +2,7 @@ enum MyError: Error { case fail + case epicFail } enum MyBigError: Error { @@ -80,3 +81,37 @@ func throwsOneOrTheOtherWithRethrow() throws { sink(be) } } + +// CHECK-LABEL: sil hidden [ossa] @$s12typed_throws0B26ConcreteWithDoCatchRethrowyyKF : $@convention(thin) () -> @error any Error +func throwsConcreteWithDoCatchRethrow() throws { + do { + // CHECK: [[FN:%[0-9]+]] = function_ref @$s12typed_throws0B8ConcreteyyKF : $@convention(thin) () -> @error MyError + // CHECK: try_apply [[FN]]() : $@convention(thin) () -> @error MyError, normal [[NORMAL_BB:bb[0-9]+]], error [[ERROR_BB:bb[0-9]+]] + try throwsConcrete() + + // CHECK: [[ERROR_BB]]([[ERROR:%[0-9]+]] : $MyError): + // CHECK-NEXT: switch_enum [[ERROR]] : $MyError, case #MyError.fail!enumelt: [[FAILCASE_BB:bb[0-9]+]], default [[DEFAULT_BB:bb[0-9]+]] + } catch .fail { + } + + // CHECK: [[DEFAULT_BB]]: + // CHECK-NOT: throw + // CHECK: alloc_existential_box $any Error + // CHECK: throw [[ERR:%[0-9]+]] : $any Error +} + +// CHECK-LABEL: sil hidden [ossa] @$s12typed_throws0B31ConcreteWithDoCatchTypedRethrowyyKF : $@convention(thin) () -> @error MyError +func throwsConcreteWithDoCatchTypedRethrow() throws(MyError) { + do { + // CHECK: [[FN:%[0-9]+]] = function_ref @$s12typed_throws0B8ConcreteyyKF : $@convention(thin) () -> @error MyError + // CHECK: try_apply [[FN]]() : $@convention(thin) () -> @error MyError, normal [[NORMAL_BB:bb[0-9]+]], error [[ERROR_BB:bb[0-9]+]] + try throwsConcrete() + + // CHECK: [[ERROR_BB]]([[ERROR:%[0-9]+]] : $MyError): + // CHECK-NEXT: switch_enum [[ERROR]] : $MyError, case #MyError.fail!enumelt: [[FAILCASE_BB:bb[0-9]+]], default [[DEFAULT_BB:bb[0-9]+]] + } catch .fail { + } + + // CHECK: [[DEFAULT_BB]]: + // CHECK-NEXT: throw [[ERROR]] : $MyError +} diff --git a/test/stmt/typed_throws.swift b/test/stmt/typed_throws.swift new file mode 100644 index 0000000000000..7696dee1c142c --- /dev/null +++ b/test/stmt/typed_throws.swift @@ -0,0 +1,121 @@ +// RUN: %target-typecheck-verify-swift -enable-experimental-feature TypedThrows + +enum MyError: Error { +case failed +case epicFailed +} + +enum HomeworkError: Error { +case dogAteIt +case forgot +} + +func processMyError(_: MyError) { } + +func doSomething() throws(MyError) { } +func doHomework() throws(HomeworkError) { } + +func testDoCatchErrorTyped() { + #if false + // FIXME: Deal with throws directly in the do...catch blocks. + do { + throw MyError.failed + } catch { + assert(error == .failed) + processMyError(error) + } + #endif + + // Throwing a typed error in a do...catch catches the error with that type. + do { + try doSomething() + } catch { + assert(error == .failed) + processMyError(error) + } + + // Throwing a typed error in a do...catch lets us pattern-match against that + // type. + do { + try doSomething() + } catch .failed { + // okay, matches one of the cases of MyError + } catch { + assert(error == .epicFailed) + } + + // Rethrowing an error because the catch is not exhaustive. + do { + try doSomething() + // expected-error@-1{{errors thrown from here are not handled because the enclosing catch is not exhaustive}} + } catch .failed { + } + + // "as X" errors are never exhaustive. + do { + try doSomething() + // FIXME: should error errors thrown from here are not handled because the enclosing catch is not exhaustive + } catch let error as MyError { // expected-warning{{'as' test is always true}} + _ = error + } + + // Rethrowing an error because the catch is not exhaustive. + do { + try doSomething() + // expected-error@-1{{errors thrown from here are not handled because the enclosing catch is not exhaustive}} + } catch is HomeworkError { + // expected-warning@-1{{cast from 'MyError' to unrelated type 'HomeworkError' always fails}} + } +} + +func testDoCatchMultiErrorType() { + // Throwing different typed errors results in 'any Error' + do { + try doSomething() + try doHomework() + } catch .failed { // expected-error{{type 'any Error' has no member 'failed'}} + + } catch { + let _: Int = error // expected-error{{cannot convert value of type 'any Error' to specified type 'Int'}} + } +} + +func testDoCatchRethrowsUntyped() throws { + do { + try doSomething() + } catch .failed { + } // okay, rethrows with a conversion to 'any Error' +} + +func testDoCatchRethrowsTyped() throws(HomeworkError) { + do { + try doHomework() + } catch .dogAteIt { + } // okay, rethrows + + do { + try doSomething() + } catch .failed { + + } // expected-error{{thrown expression type 'MyError' cannot be converted to error type 'HomeworkError'}} + + do { + try doSomething() + try doHomework() + } catch let e as HomeworkError { + _ = e + } // expected-error{{thrown expression type 'any Error' cannot be converted to error type 'HomeworkError'}} + + do { + try doSomething() + try doHomework() + } catch { + + } // okay, the thrown 'any Error' has been caught +} + +func testTryIncompatibleTyped() throws(HomeworkError) { + try doHomework() // okay + + try doSomething() // FIXME: should error +}