Skip to content

[Typed throws] Compute and use the caught error type of a do..catch block #68976

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions include/swift/AST/Stmt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
9 changes: 9 additions & 0 deletions lib/AST/Stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions lib/SILGen/SILGenStmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 43 additions & 1 deletion lib/Sema/TypeCheckEffects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 17 additions & 1 deletion lib/Sema/TypeCheckStmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1678,10 +1678,26 @@ class StmtChecker : public StmtVisitor<StmtChecker, Stmt*> {
// 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;
}
Expand Down
7 changes: 7 additions & 0 deletions lib/Sema/TypeChecker.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
35 changes: 35 additions & 0 deletions test/SILGen/typed_throws.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

enum MyError: Error {
case fail
case epicFail
}

enum MyBigError: Error {
Expand Down Expand Up @@ -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
}
121 changes: 121 additions & 0 deletions test/stmt/typed_throws.swift
Original file line number Diff line number Diff line change
@@ -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
}