Skip to content

Sendable checking for an an isolated witnesses to a nonisolated requirement #40560

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
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
7 changes: 7 additions & 0 deletions lib/Sema/TypeCheckConcurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3266,6 +3266,13 @@ ActorIsolation ActorIsolationRequest::evaluate(
ASTContext &ctx = value->getASTContext();
switch (inferred) {
case ActorIsolation::Independent:
// Stored properties cannot be non-isolated, so don't infer it.
if (auto var = dyn_cast<VarDecl>(value)) {
if (!var->isStatic() && var->hasStorage())
return ActorIsolation::forUnspecified();
}


if (onlyGlobal)
return ActorIsolation::forUnspecified();

Expand Down
35 changes: 33 additions & 2 deletions lib/Sema/TypeCheckProtocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2862,6 +2862,31 @@ static void emitDeclaredHereIfNeeded(DiagnosticEngine &diags,
diags.diagnose(value, diag::decl_declared_here, value->getName());
}

/// Determine if the witness can be made implicitly async.
static bool isValidImplicitAsync(ValueDecl *witness, ValueDecl *requirement) {
if (auto witnessFunc = dyn_cast<AbstractFunctionDecl>(witness)) {
if (auto requirementFunc = dyn_cast<AbstractFunctionDecl>(requirement)) {
return requirementFunc->hasAsync() &&
!(witnessFunc->hasThrows() && !requirementFunc->hasThrows());
}

return false;
}

if (auto witnessStorage = dyn_cast<AbstractStorageDecl>(witness)) {
if (auto requirementStorage = dyn_cast<AbstractStorageDecl>(requirement)) {
auto requirementAccessor = requirementStorage->getEffectfulGetAccessor();
if (!requirementAccessor || !requirementAccessor->hasAsync())
return false;

return witnessStorage->isLessEffectfulThan(
requirementStorage, EffectKind::Throws);
}
}

return false;
}

bool ConformanceChecker::checkActorIsolation(
ValueDecl *requirement, ValueDecl *witness) {
/// Retrieve a concrete witness for Sendable checking.
Expand Down Expand Up @@ -2986,9 +3011,15 @@ bool ConformanceChecker::checkActorIsolation(
// A synchronous actor function can witness an asynchronous protocol
// requirement, since calls "through" the protocol are always cross-actor,
// in which case the function becomes implicitly async.
//
// In this case, we're crossing actor boundaries so we need to
// perform Sendable checking.
if (witnessClass && witnessClass->isActor()) {
if (requirementFunc && requirementFunc->hasAsync() &&
(requirementFunc->hasThrows() == witnessFunc->hasThrows())) {
if (isValidImplicitAsync(witness, requirement)) {
diagnoseNonSendableTypesInReference(
getConcreteWitness(), DC, witness->getLoc(),
ConcurrentReferenceKind::CrossActor);

return false;
}
}
Expand Down
4 changes: 2 additions & 2 deletions test/Concurrency/actor_isolation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -967,13 +967,13 @@ func testCrossActorProtocol<T: P>(t: T) async {

@available(SwiftStdlib 5.1, *)
protocol Server {
func send<Message: Codable>(message: Message) async throws -> String
func send<Message: Codable & Sendable>(message: Message) async throws -> String
}

@available(SwiftStdlib 5.1, *)
actor MyServer : Server {
// okay, asynchronously accessed from clients of the protocol
func send<Message: Codable>(message: Message) throws -> String { "" }
func send<Message: Codable & Sendable>(message: Message) throws -> String { "" }
}

// ----------------------------------------------------------------------
Expand Down
11 changes: 11 additions & 0 deletions test/Concurrency/global_actor_inference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ func testNotAllInP1(nap1: NotAllInP1) { // expected-note{{add '@SomeGlobalActor'
nap1.other() // okay
}

// Make sure we don't infer 'nonisolated' for stored properties.
@MainActor
protocol Interface {
nonisolated var baz: Int { get } // expected-note{{'baz' declared here}}
}

@MainActor
class Object: Interface {
var baz: Int = 42 // expected-warning{{property 'baz' isolated to global actor 'MainActor' can not satisfy corresponding requirement from protocol 'Interface'}}
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the fix here, explicitly annotating it with @mainactor? If so, can we get a fixit?

Copy link
Member Author

Choose a reason for hiding this comment

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

The fix here would have to be to turn it into a non-isolated computed property.

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, makes sense 👍 Could also be a fixit i guess

}


// ----------------------------------------------------------------------
// Global actor inference for classes and extensions
Expand Down
147 changes: 147 additions & 0 deletions test/Concurrency/sendable_conformance_checking.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// RUN: %target-typecheck-verify-swift
// REQUIRES: concurrency

@available(SwiftStdlib 5.1, *)
class NotSendable { // expected-note 8{{class 'NotSendable' does not conform to the 'Sendable' protocol}}
}

@available(SwiftStdlib 5.1, *)
@available(*, unavailable)
extension NotSendable: Sendable { }

@available(SwiftStdlib 5.1, *)
protocol IsolatedWithNotSendableRequirements: Actor {
func f() -> NotSendable
var prop: NotSendable { get }
}

// Okay, everything is isolated the same way
@available(SwiftStdlib 5.1, *)
actor A1: IsolatedWithNotSendableRequirements {
func f() -> NotSendable { NotSendable() }
var prop: NotSendable { NotSendable() }
}

// Okay, sendable checking occurs when calling through the protocol
// and also inside the bodies.
@available(SwiftStdlib 5.1, *)
actor A2: IsolatedWithNotSendableRequirements {
nonisolated func f() -> NotSendable { NotSendable() }
nonisolated var prop: NotSendable { NotSendable() }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

so in this case we get warnings on A2().f() ?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, we don't need warnings on A2().f() because it doesn't cross any actor boundaries.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah because nonisolated, right... Thanks!


@available(SwiftStdlib 5.1, *)
protocol AsyncProtocolWithNotSendable {
func f() async -> NotSendable
var prop: NotSendable { get async }
}

// Sendable checking required because calls through protocol cross into the
// actor's domain.
@available(SwiftStdlib 5.1, *)
actor A3: AsyncProtocolWithNotSendable {
func f() async -> NotSendable { NotSendable() } // expected-warning{{cannot call function returning non-sendable type 'NotSendable' across actors}}
Copy link
Contributor

Choose a reason for hiding this comment

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

the error message is super confusing here...? There is no call here 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, we aren't customizing these diagnostics well as a general thing.


var prop: NotSendable { // expected-warning{{cannot use property 'prop' with a non-sendable type 'NotSendable' across actors}}
get async {
NotSendable()
}
}
}

// Sendable checking required because calls through protocol cross into the
// actor's domain.
@available(SwiftStdlib 5.1, *)
actor A4: AsyncProtocolWithNotSendable {
func f() -> NotSendable { NotSendable() } // expected-warning{{cannot call function returning non-sendable type 'NotSendable' across actors}}

var prop: NotSendable { // expected-warning{{cannot use property 'prop' with a non-sendable type 'NotSendable' across actors}}
get {
NotSendable()
}
}
}

// Sendable checking not required because we never cross into the actor's
// domain.
@available(SwiftStdlib 5.1, *)
actor A5: AsyncProtocolWithNotSendable {
nonisolated func f() async -> NotSendable { NotSendable() }

nonisolated var prop: NotSendable {
get async {
NotSendable()
}
}
}

// Sendable checking not required because we never cross into the actor's
// domain.
@available(SwiftStdlib 5.1, *)
actor A6: AsyncProtocolWithNotSendable {
nonisolated func f() -> NotSendable { NotSendable() }

nonisolated var prop: NotSendable {
get {
NotSendable()
}
}
}

@available(SwiftStdlib 5.1, *)
protocol AsyncThrowingProtocolWithNotSendable {
func f() async throws -> NotSendable
var prop: NotSendable { get async throws }
}

// Sendable checking required because calls through protocol cross into the
// actor's domain.
@available(SwiftStdlib 5.1, *)
actor A7: AsyncThrowingProtocolWithNotSendable {
func f() async -> NotSendable { NotSendable() } // expected-warning{{cannot call function returning non-sendable type 'NotSendable' across actors}}

var prop: NotSendable { // expected-warning{{cannot use property 'prop' with a non-sendable type 'NotSendable' across actors}}
get async {
NotSendable()
}
}
}

// Sendable checking required because calls through protocol cross into the
// actor's domain.
@available(SwiftStdlib 5.1, *)
actor A8: AsyncThrowingProtocolWithNotSendable {
func f() -> NotSendable { NotSendable() } // expected-warning{{cannot call function returning non-sendable type 'NotSendable' across actors}}

var prop: NotSendable { // expected-warning{{cannot use property 'prop' with a non-sendable type 'NotSendable' across actors}}
get {
NotSendable()
}
}
}

// Sendable checking not required because we never cross into the actor's
// domain.
@available(SwiftStdlib 5.1, *)
actor A9: AsyncThrowingProtocolWithNotSendable {
nonisolated func f() async -> NotSendable { NotSendable() }

nonisolated var prop: NotSendable {
get async {
NotSendable()
}
}
}

// Sendable checking not required because we never cross into the actor's
// domain.
@available(SwiftStdlib 5.1, *)
actor A10: AsyncThrowingProtocolWithNotSendable {
nonisolated func f() -> NotSendable { NotSendable() }

nonisolated var prop: NotSendable {
get {
NotSendable()
}
}
}
2 changes: 1 addition & 1 deletion test/Distributed/distributed_protocol_isolation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ protocol Server {
func send<Message: Codable>(message: Message) async throws -> String
}
actor MyServer : Server {
func send<Message: Codable>(message: Message) throws -> String { "" } // okay, asynchronously accessed from clients of the protocol
func send<Message: Codable>(message: Message) throws -> String { "" } // expected-warning{{cannot pass argument of non-sendable type 'Message' across actors}}
}

protocol AsyncThrowsAll {
Expand Down