Skip to content

[Distributed] Fix when we invoke thunk, base it on typesystem implicit effects #39542

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 2 commits into from
Oct 2, 2021
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
20 changes: 16 additions & 4 deletions include/swift/AST/Expr.h
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,13 @@ class alignas(8) Expr : public ASTAllocated<Expr> {
NumCaptures : 32
);

SWIFT_INLINE_BITFIELD(ApplyExpr, Expr, 1+1+1+1+1,
SWIFT_INLINE_BITFIELD(ApplyExpr, Expr, 1+1+1+1+1+1,
ThrowsIsSet : 1,
Throws : 1,
ImplicitlyAsync : 1,
ImplicitlyThrows : 1,
NoAsync : 1
NoAsync : 1,
ShouldApplyDistributedThunk : 1
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be set to false in the ApplyExpr constructor, or you'll end up with spuriously-distributed calls due to uninitialized memory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh man, thanks I totally forgot about that 😱

);

SWIFT_INLINE_BITFIELD_EMPTY(CallExpr, ApplyExpr);
Expand Down Expand Up @@ -1206,7 +1207,9 @@ class DeclRefExpr : public Expr {
/// which are cross-actor invoked, because such calls actually go over the
/// transport/network, and may throw from this, rather than the function
/// implementation itself..
bool isImplicitlyThrows() const { return Bits.DeclRefExpr.IsImplicitlyThrows; }
bool isImplicitlyThrows() const {
return Bits.DeclRefExpr.IsImplicitlyThrows;
}

/// Set whether this reference must account for a `throw` occurring for reasons
/// other than the function implementation itself throwing, e.g. an
Expand Down Expand Up @@ -4391,7 +4394,7 @@ class ApplyExpr : public Expr {
///
/// where the new closure is declared to be async.
///
/// When the application is implciitly async, the result describes
/// When the application is implicitly async, the result describes
/// the actor to which we need to need to hop.
Optional<ImplicitActorHopTarget> isImplicitlyAsync() const {
if (!Bits.ApplyExpr.ImplicitlyAsync)
Expand Down Expand Up @@ -4419,6 +4422,15 @@ class ApplyExpr : public Expr {
Bits.ApplyExpr.ImplicitlyThrows = flag;
}

/// Informs IRGen to that this expression should be applied as its distributed
/// thunk, rather than invoking the function directly.
bool shouldApplyDistributedThunk() const {
return Bits.ApplyExpr.ShouldApplyDistributedThunk;
}
void setShouldApplyDistributedThunk(bool flag) {
Bits.ApplyExpr.ShouldApplyDistributedThunk = flag;
}

ValueDecl *getCalledValue() const;

static bool classof(const Expr *E) {
Expand Down
8 changes: 2 additions & 6 deletions lib/SILGen/SILGenApply.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1117,12 +1117,8 @@ class SILGenApply : public Lowering::ExprVisitor<SILGenApply> {

// Otherwise, we have a statically-dispatched call.
auto constant = SILDeclRef(e->getDecl());
if (e->getDecl()->getAttrs().hasAttribute<DistributedActorAttr>()) {
// we're calling a distributed function, only in cross-actor situations
// does this actually need to call the thunk.
if (!selfApply) {
constant = constant.asDistributed(true);
}
if (callSite && callSite->shouldApplyDistributedThunk()) {
constant = constant.asDistributed(true);
} else {
constant = constant.asForeign(
!isConstructorWithGeneratedAllocatorThunk(e->getDecl())
Expand Down
22 changes: 15 additions & 7 deletions lib/Sema/TypeCheckConcurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1003,14 +1003,16 @@ namespace {
///
/// and we reach up to mark the CallExpr.
void markNearestCallAsImplicitly(
Optional<ImplicitActorHopTarget> setAsync, bool setThrows = false) {
Optional<ImplicitActorHopTarget> setAsync,
bool setThrows = false, bool setDistributedThunk = false) {
assert(applyStack.size() > 0 && "not contained within an Apply?");

const auto End = applyStack.rend();
for (auto I = applyStack.rbegin(); I != End; ++I)
if (auto call = dyn_cast<CallExpr>(*I)) {
if (setAsync) call->setImplicitlyAsync(*setAsync);
if (setThrows) call->setImplicitlyThrows(true);
if (setDistributedThunk) call->setShouldApplyDistributedThunk(true);
return;
}
llvm_unreachable("expected a CallExpr in applyStack!");
Expand Down Expand Up @@ -1667,7 +1669,6 @@ namespace {
ThrowsMarkingResult tryMarkImplicitlyThrows(SourceLoc declLoc,
ConcreteDeclRef concDeclRef,
Expr* context) {

ValueDecl *decl = concDeclRef.getDecl();
ThrowsMarkingResult result = ThrowsMarkingResult::NotFound;

Expand Down Expand Up @@ -1869,8 +1870,11 @@ namespace {
}

switch (contextIsolation) {
case ActorIsolation::ActorInstance:
case ActorIsolation::DistributedActorInstance: {
case ActorIsolation::DistributedActorInstance:
markNearestCallAsImplicitly(/*setAsync*/None, /*setThrows*/false,
/*setDistributedThunk*/true);
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this. It doesn't seem like setThrows can ever be false while setDistributedThunk is true, because you can't do a distributed call without the potential for an error to be thrown.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I think we can collapse the two - an implicit throws (currently) always implies that it's a distributed call. I wasn't sure if we want to keep it separate, technically there could be other reasons for an implicit throw in the future I guess...?

We also need to cause the think even if the implicit throws is not in effect because the func is throwing anyway

LLVM_FALLTHROUGH;
case ActorIsolation::ActorInstance: {
auto result = tryMarkImplicitlyAsync(
loc, valueRef, context,
ImplicitActorHopTarget::forGlobalActor(globalActor));
Expand Down Expand Up @@ -2277,7 +2281,10 @@ namespace {
} else if (func->isDistributed()) {
tryMarkImplicitlyAsync(memberLoc, memberRef, context,
ImplicitActorHopTarget::forInstanceSelf());
tryMarkImplicitlyThrows(memberLoc, memberRef, context);
tryMarkImplicitlyThrows(memberLoc, memberRef, context);
markNearestCallAsImplicitly(/*setAsync*/None, /*setThrows*/false,
Copy link
Member

Choose a reason for hiding this comment

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

Oh, this is because the tryMarkImplicitThrows is separate. Huh.

/*setDistributedThunk*/true);

} else {
// neither static or distributed, apply full distributed isolation
ctx.Diags.diagnose(memberLoc, diag::distributed_actor_isolated_method)
Expand Down Expand Up @@ -2362,8 +2369,7 @@ namespace {
}

// It wasn't a distributed func, so ban the access
// TODO(distributed): handle subscripts here too
if (auto var = dyn_cast<VarDecl>(member)) {
if (isPropOrSubscript(member)) {
ctx.Diags.diagnose(
memberLoc, diag::distributed_actor_isolated_non_self_reference,
member->getDescriptiveKind(), member->getName());
Expand All @@ -2379,6 +2385,8 @@ namespace {
if (isolation.getActorType()->isDistributedActor() &&
!isolatedActor.isPotentiallyIsolated) {
tryMarkImplicitlyThrows(memberLoc, memberRef, context);
markNearestCallAsImplicitly(/*setAsync*/None, /*setThrows*/false,
/*setDistributedThunk*/true);
}

if (implicitAsyncResult == AsyncMarkingResult::FoundAsync)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct FakeTransport: ActorTransport {
fatalError("not implemented:\(#function)")
}

func resolve<Act>(_ identity: Act.ID, as actorType: Act.Type)
func resolve<Act>(_ identity: AnyActorIdentity, as actorType: Act.Type)
throws -> Act?
where Act: DistributedActor {
return nil
Expand Down Expand Up @@ -95,7 +95,7 @@ func test_remote() async throws {
let address = ActorAddress(parse: "")
let transport = FakeTransport()

let worker = try LocalWorker(resolve: .init(address), using: transport)
let worker = try LocalWorkerresolve(.init(address), using: transport)
let x = try await worker.function()
print("call: \(x)")
// CHECK: call: _cluster_remote_function():
Expand Down
4 changes: 2 additions & 2 deletions test/Distributed/Runtime/distributed_actor_local.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ struct FakeTransport: ActorTransport {
fatalError("not implemented \(#function)")
}

func resolve<Act>(_ identity: Act.ID, as actorType: Act.Type) throws -> Act?
func resolve<Act>(_ identity: AnyActorIdentity, as actorType: Act.Type) throws -> Act?
where Act: DistributedActor {
return nil
}
Expand All @@ -75,7 +75,7 @@ func test_initializers() {
let transport = FakeTransport()

_ = SomeSpecificDistributedActor(transport: transport)
_ = try! SomeSpecificDistributedActor(resolve: .init(address), using: transport)
_ = try! SomeSpecificDistributedActor.resolve(.init(address), using: transport)
}

@available(SwiftStdlib 5.5, *)
Expand Down
69 changes: 57 additions & 12 deletions test/Distributed/Runtime/distributed_actor_remote_functions.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// RUN: %target-run-simple-swift(-Xfrontend -enable-experimental-distributed -parse-as-library) | %FileCheck %s --dump-input=always
// RUN: %target-run-simple-swift(-Xfrontend -disable-availability-checking -Xfrontend -enable-experimental-distributed -parse-as-library) | %FileCheck %s --dump-input=always

// REQUIRES: executable_test
// REQUIRES: concurrency
Expand All @@ -11,8 +11,6 @@
// rdar://77798215
// UNSUPPORTED: OS=windows-msvc

// REQUIRES: rdar78290608
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we enable all of them in #38914


import _Distributed
import _Concurrency

Expand Down Expand Up @@ -43,6 +41,31 @@ distributed actor SomeSpecificDistributedActor {
"local(\(#function))"
}

distributed func callTaskSelf_inner() async throws -> String {
"local(\(#function))"
}
distributed func callTaskSelf() async -> String {
do {
return try await Task {
let called = try await callTaskSelf_inner() // shouldn't use the distributed thunk!
return "local(\(#function)) -> \(called)"
}.value
} catch {
return "WRONG local(\(#function)) thrown(\(error))"
}
}

distributed func callDetachedSelf() async -> String {
do {
return try await Task.detached {
let called = try await self.callTaskSelf_inner() // shouldn't use the distributed thunk!
return "local(\(#function)) -> \(called)"
}.value
} catch {
return "WRONG local(\(#function)) thrown(\(error))"
}
}

// === errors

distributed func helloThrowsImplBoom() throws -> String {
Expand Down Expand Up @@ -76,6 +99,21 @@ extension SomeSpecificDistributedActor {
"remote(\(#function))"
}

@_dynamicReplacement(for:_remote_callTaskSelf())
nonisolated func _remote_impl_callTaskSelf() async throws -> String {
"remote(\(#function))"
}

@_dynamicReplacement(for:_remote_callDetachedSelf())
nonisolated func _remote_impl_callDetachedSelf() async throws -> String {
"remote(\(#function))"
}

@_dynamicReplacement(for:_remote_callTaskSelf_inner())
nonisolated func _remote_impl_callTaskSelf_inner() async throws -> String {
"remote(\(#function))"
}

// === errors

@_dynamicReplacement(for:_remote_helloThrowsImplBoom())
Expand Down Expand Up @@ -103,9 +141,6 @@ func __isLocalActor(_ actor: AnyObject) -> Bool {
@available(SwiftStdlib 5.5, *)
struct ActorAddress: ActorIdentity {
let address: String
init(parse address : String) {
self.address = address
}
}

@available(SwiftStdlib 5.5, *)
Expand All @@ -114,15 +149,15 @@ struct FakeTransport: ActorTransport {
fatalError("not implemented:\(#function)")
}

func resolve<Act>(_ identity: Act.ID, as actorType: Act.Type)
func resolve<Act>(_ identity: AnyActorIdentity, as actorType: Act.Type)
throws -> Act?
where Act: DistributedActor {
return nil
}

func assignIdentity<Act>(_ actorType: Act.Type) -> AnyActorIdentity
where Act: DistributedActor {
.init(ActorAddress(parse: ""))
.init(ActorAddress(address: ""))
}

func actorReady<Act>(_ actor: Act) where Act: DistributedActor {
Expand Down Expand Up @@ -151,25 +186,31 @@ func test_remote_invoke(address: ActorAddress, transport: ActorTransport) async
let h4 = try! await actor.hello()
print("\(personality) - hello: \(h4)")

let h5 = try! await actor.callTaskSelf()
print("\(personality) - callTaskSelf: \(h5)")

let h6 = try! await actor.callDetachedSelf()
print("\(personality) - callDetachedSelf: \(h6)")

// error throws
if __isRemoteActor(actor) {
do {
_ = try await actor.helloThrowsTransportBoom()
preconditionFailure("helloThrowsTransportBoom: should have thrown")
print("WRONG: helloThrowsTransportBoom: should have thrown")
} catch {
print("\(personality) - helloThrowsTransportBoom: \(error)")
}
} else {
do {
_ = try await actor.helloThrowsImplBoom()
preconditionFailure("helloThrowsImplBoom: Should have thrown")
print("WRONG: helloThrowsImplBoom: Should have thrown")
} catch {
print("\(personality) - helloThrowsImplBoom: \(error)")
}
}
}

let remote = try! SomeSpecificDistributedActor(resolve: .init(address), using: transport)
let remote = try! SomeSpecificDistributedActor.resolve(.init(address), using: transport)
assert(__isRemoteActor(remote) == true, "should be remote")

let local = SomeSpecificDistributedActor(transport: transport)
Expand All @@ -182,6 +223,8 @@ func test_remote_invoke(address: ActorAddress, transport: ActorTransport) async
// CHECK: local - helloAsync: local(helloAsync())
// CHECK: local - helloThrows: local(helloThrows())
// CHECK: local - hello: local(hello())
// CHECK: local - callTaskSelf: local(callTaskSelf()) -> local(callTaskSelf_inner())
// CHECK: local - callDetachedSelf: local(callDetachedSelf()) -> local(callTaskSelf_inner())
// CHECK: local - helloThrowsImplBoom: Boom(whoFailed: "impl")

print("remote isRemote: \(__isRemoteActor(remote))")
Expand All @@ -191,6 +234,8 @@ func test_remote_invoke(address: ActorAddress, transport: ActorTransport) async
// CHECK: remote - helloAsync: remote(_remote_impl_helloAsync())
// CHECK: remote - helloThrows: remote(_remote_impl_helloThrows())
// CHECK: remote - hello: remote(_remote_impl_hello())
// CHECK: remote - callTaskSelf: remote(_remote_impl_callTaskSelf())
// CHECK: remote - callDetachedSelf: remote(_remote_impl_callDetachedSelf())
// CHECK: remote - helloThrowsTransportBoom: Boom(whoFailed: "transport")

print(local)
Expand All @@ -200,7 +245,7 @@ func test_remote_invoke(address: ActorAddress, transport: ActorTransport) async
@available(SwiftStdlib 5.5, *)
@main struct Main {
static func main() async {
let address = ActorAddress(parse: "")
let address = ActorAddress(address: "")
let transport = FakeTransport()

await test_remote_invoke(address: address, transport: transport)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct FakeTransport: ActorTransport {
fatalError("not implemented:\(#function)")
}

func resolve<Act>(_ identity: Act.ID, as actorType: Act.Type)
func resolve<Act>(_ identity: AnyActorIdentity, as actorType: Act.Type)
throws -> Act?
where Act: DistributedActor {
return nil
Expand Down Expand Up @@ -65,7 +65,7 @@ func test_remote() async {
let address = ActorAddress(parse: "")
let transport = FakeTransport()

let remote = try! SomeSpecificDistributedActor(resolve: .init(address), using: transport)
let remote = try! SomeSpecificDistributedActor.resolve(.init(address), using: transport)
_ = try! await remote.hello() // let it crash!

// CHECK: SOURCE_DIR/test/Distributed/Runtime/distributed_no_transport_boom.swift:18: Fatal error: Invoked remote placeholder function '_remote_hello' on remote distributed actor of type 'main.SomeSpecificDistributedActor'. Configure an appropriate 'ActorTransport' for this actor to resolve this error (e.g. by depending on some specific transport library).
Expand Down