Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
3 changes: 3 additions & 0 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -5461,6 +5461,9 @@ WARNING(no_throw_in_do_with_catch,none,
ERROR(thrown_type_not_error,none,
"thrown type %0 does not conform to the 'Error' protocol", (Type))

ERROR(typed_thrown_in_objc_forbidden,none,
"@objc functions cannot have typed throw", ())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably word this more similarly to the other diagnostics emitted by isRepresentableInLanguage(), such as not_objc_function_async:

Suggested change
"@objc functions cannot have typed throw", ())
"typed 'throws' %kindonly0 cannot be represented in Objective-C",
(const AbstractFunctionDecl *))

Note that this will insert a string like "instance method" where %kindonly0 is written—you'll need to pass the decl into the diagnose() call, though.

(There are other ways you might word this—if you'd like, take a look around at a few other diagnostics in this method and see if there's something you like better.)


//------------------------------------------------------------------------------
// MARK: Concurrency
//------------------------------------------------------------------------------
Expand Down
20 changes: 20 additions & 0 deletions lib/Sema/TypeCheckDeclObjC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,26 @@ bool swift::isRepresentableInLanguage(
return false;
}
}
auto isTypedThrow = [&]() {
// With no AST token this should be `throws`
if (!AFD->getThrownTypeRepr())
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a subtle pitfall here: A TypeRepr is an exact representation of how a type is written in source code, so it's only available when the Swift compiler has directly parsed source code to create the declaration. If a declaration uses typed throws but it was deserialized from a .swiftmodule file or synthesized by the compiler, then getThrownTypeRepr() will return nullptr even though there is actually a typed throws.

The various methods that return the thrown type as a Type, rather than a TypeRepr *, don't have this problem, so you can just remove the getThrownTypeRepr() check—checking getThrownInterfaceType() alone will tell you everything you need to know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fairly new to swift, can you elaborate more on "If a declaration uses typed throws but it was deserialized from a .swiftmodule file or synthesized by the compiler", how to create either case, and how are they used in practice?

// Or it is a `throws(<ErrorType>)`
CanType thrownType = AFD->getThrownInterfaceType()->getCanonicalType();
// TODO: only `throws(Error)` is allowed.
// Throwing `any MyError` that confronts `Error` is not implemented yet.
// Shall we allow `any MyError` in the future, we should check against
// `isExistentialType` instead.
if (thrownType->isErrorExistentialType())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Digging into the implementation underlying getThrownInterfaceType(), it turns out that if a function is non-throwing, it will return the type for Swift.Never. So if you wrote this condition:

Suggested change
if (thrownType->isErrorExistentialType())
if (thrownType->isNever() || thrownType->isErrorExistentialType())

You would not need to explicitly check hasThrows() before calling your isTypedThrow() function.

(If you don't do the isNever() check, you might want to remove the call to getCanonicalType() on line 817—TypeBase::isErrorExistentialType() implicitly canonicalizes the type, so you don't have to.)

Copy link
Contributor Author

@DataCorrupted DataCorrupted Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thrownType could be a null pointer if not getCanonicalType, even if AFD hasThrow. I'm not sure why.

I'd prefer keep the hasThrow check to make the intention more clear, it also made more sense to erase the closure that this point.

return false;
softenIfAccessNote(AFD, Reason.getAttr(),
AFD->diagnose(diag::typed_thrown_in_objc_forbidden)
.limitBehavior(behavior));
Reason.describe(AFD);
return true;
};
if (AFD->hasThrows() && isTypedThrow())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, checking this before the sync and async code paths diverge is clever!

return false;

if (AFD->hasAsync()) {
// Asynchronous functions move all of the result value and thrown error
Expand Down
12 changes: 12 additions & 0 deletions test/attr/attr_objc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,11 @@ extension Protocol_ObjC1 {
//===---
//===--- Error handling
//===---
enum ObjCError: Int, Error {
case Others
}
protocol MyError: Error { }

// CHECK-LABEL: class ClassThrows1
class ClassThrows1 {
// CHECK: @objc func methodReturnsVoid() throws
Expand Down Expand Up @@ -2329,6 +2334,13 @@ class ClassThrows1 {

// CHECK: {{^}} func fooWithErrorProtocolComposition2(x: any Error & Protocol_ObjC1)
func fooWithErrorProtocolComposition2(x: Error & Protocol_ObjC1) { }

@objc func throwsError() throws(Error) {}
@objc func throwsMyError() throws(MyError) {}
// expected-error@-1{{@objc functions cannot have typed throw}}
// expected-error@-2{{thrown type 'any MyError' does not conform to the 'Error' protocol}}
@objc func throwsObjCError() throws(ObjCError) {}
// expected-error@-1{{@objc functions cannot have typed throw}}
}


Expand Down
8 changes: 8 additions & 0 deletions test/attr/attr_objc_async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class MyClass {

// actor exporting Objective-C entry points.

enum ObjCError: Int, Error {
case Others
}

// CHECK: actor MyActor
actor MyActor {
// CHECK: @objc func doBigJobActor() async -> Int
Expand All @@ -49,6 +53,10 @@ actor MyActor {

// CHECK: @objc nonisolated func synchronousGood()
@objc nonisolated func synchronousGood() { }

@objc func objcAsyncThrowsError() async throws(Error) -> Void {}
@objc func objcAsyncThrowsObjCError() async throws(ObjCError) -> Void {}
// expected-error@-1 {{@objc functions cannot have typed throw}}
}

actor class MyActor2 { }
Expand Down