Skip to content

[AST/Sema] Add new function type isolation - caller to cover @execution(caller) attribute #79375

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 4 commits into from
Feb 15, 2025
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
18 changes: 18 additions & 0 deletions include/swift/AST/ExtInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ class FunctionTypeIsolation {

/// The function's isolation is statically erased with @isolated(any).
Erased,

/// Inherits isolation from the caller. This is only applicable
/// to asynchronous function types.
///
/// NOTE: The difference in between NonIsolatedCaller and
/// NonIsolated is that NonIsolatedCaller is a strictly
/// weaker form of nonisolation. While both in their bodies cannot
/// access isolated state directly, NonIsolatedCaller functions
/// /are/ allowed to access state isolated to their caller via
/// function arguments since we know that the callee will stay
/// in the caller's isolation domain. In contrast, NonIsolated
/// is strongly nonisolated and is not allowed to access /any/
/// isolated state (even via function parameters) since it is
/// considered safe to run on /any/ actor.
NonIsolatedCaller,
};

static constexpr size_t NumBits = 3; // future-proof this slightly
Expand All @@ -87,6 +102,9 @@ class FunctionTypeIsolation {
static FunctionTypeIsolation forErased() {
return { Kind::Erased };
}
static FunctionTypeIsolation forNonIsolatedCaller() {
return { Kind::NonIsolatedCaller };
}

Kind getKind() const { return value.getInt(); }
bool isNonIsolated() const {
Expand Down
5 changes: 5 additions & 0 deletions lib/AST/ASTMangler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3285,6 +3285,11 @@ void ASTMangler::appendFunctionSignature(AnyFunctionType *fn,
if (AllowIsolatedAny)
appendOperator("YA");
break;

case FunctionTypeIsolation::Kind::NonIsolatedCaller:
// TODO: We need a special mangling for this to
// make it distinct from the `@execution(concurrent)`.
break;
}

if (isRecursedInto && fn->hasSendingResult()) {
Expand Down
4 changes: 4 additions & 0 deletions lib/AST/ASTPrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6421,6 +6421,10 @@ class TypePrinter : public TypeVisitor<TypePrinter> {
if (!Options.SuppressIsolatedAny)
Printer << "@isolated(any) ";
break;

case FunctionTypeIsolation::Kind::NonIsolatedCaller:
Printer << "@execution(caller) ";
break;
}

if (!Options.excludeAttrKind(TypeAttrKind::Sendable) && info.isSendable()) {
Expand Down
1 change: 1 addition & 0 deletions lib/SILGen/SILGenConcurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ SILGenFunction::emitFunctionTypeIsolation(SILLocation loc,

// Emit nonisolated by simply emitting Optional.none in the result type.
case FunctionTypeIsolation::Kind::NonIsolated:
case FunctionTypeIsolation::Kind::NonIsolatedCaller:
return emitNonIsolatedIsolation(loc);

// Emit global actor isolation by loading .shared from the global actor,
Expand Down
4 changes: 4 additions & 0 deletions lib/SILGen/SILGenPoly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5439,6 +5439,10 @@ static void buildThunkBody(SILGenFunction &SGF, SILLocation loc,
case FunctionTypeIsolation::Kind::NonIsolated:
break;

case FunctionTypeIsolation::Kind::NonIsolatedCaller:
hopToIsolatedParameter = true;
break;

// For a function with parameter isolation, we'll have to dig the
// argument out after translation but before making the call.
case FunctionTypeIsolation::Kind::Parameter:
Expand Down
47 changes: 47 additions & 0 deletions lib/Sema/CSSimplify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2964,6 +2964,10 @@ ConstraintSystem::matchFunctionIsolations(FunctionType *func1,
case FunctionTypeIsolation::Kind::NonIsolated:
return true;

// A thunk is going to pass `nil` to the isolated parameter.
case FunctionTypeIsolation::Kind::NonIsolatedCaller:
return matchIfConversion();

// Erasing global-actor isolation to non-isolation can admit data
// races; such violations are diagnosed by the actor isolation checker.
// We deliberately do not allow actor isolation violations to influence
Expand All @@ -2984,6 +2988,35 @@ ConstraintSystem::matchFunctionIsolations(FunctionType *func1,
}
llvm_unreachable("bad kind");

// Converting to a caller isolated async function type.
case FunctionTypeIsolation::Kind::NonIsolatedCaller:
switch (isolation1.getKind()) {
// Exact match.
case FunctionTypeIsolation::Kind::NonIsolatedCaller:
return true;

// Global actor: Thunk will hop to the global actor
// and would ignore passed in isolation.
// Erased: Just like global actor but would hop to
// the isolation stored in the @isolated(any) function.
case FunctionTypeIsolation::Kind::GlobalActor:
case FunctionTypeIsolation::Kind::Erased:
return matchIfConversion();

// In this case the isolation is dependent on a
// specific actor passed in as the isolation parameter
// and the thunk won't have it.
case FunctionTypeIsolation::Kind::Parameter:
return false;

// For asynchronous: Thunk would hop the appropriate actor.
// For synchronous: Thunk would call the function without
// a hop.
case FunctionTypeIsolation::Kind::NonIsolated:
return matchIfConversion();
}
llvm_unreachable("bad kind");

// Converting to a global-actor-isolated type.
case FunctionTypeIsolation::Kind::GlobalActor:
switch (isolation1.getKind()) {
Expand All @@ -3004,6 +3037,11 @@ ConstraintSystem::matchFunctionIsolations(FunctionType *func1,
case FunctionTypeIsolation::Kind::NonIsolated:
return matchIfConversion();

// A thunk is going to pass in an instance of a global actor
// to the isolated parameter.
case FunctionTypeIsolation::Kind::NonIsolatedCaller:
return matchIfConversion();

// Parameter isolation cannot be altered in the same way.
case FunctionTypeIsolation::Kind::Parameter:
return false;
Expand All @@ -3030,6 +3068,10 @@ ConstraintSystem::matchFunctionIsolations(FunctionType *func1,
case FunctionTypeIsolation::Kind::GlobalActor:
return matchIfConversion();

// A thunk is going to forward the isolation.
case FunctionTypeIsolation::Kind::NonIsolatedCaller:
return matchIfConversion();

// Don't allow dynamically-isolated function types to convert to
// any specific isolation for the same policy reasons that we don't
// want to allow global-actors to change.
Expand All @@ -3050,6 +3092,11 @@ ConstraintSystem::matchFunctionIsolations(FunctionType *func1,
case FunctionTypeIsolation::Kind::GlobalActor:
return matchIfConversion(/*erasure*/ true);

// It's not possible to form a thunk for this case because
// we don't know what to pass to the isolated parameter.
case FunctionTypeIsolation::Kind::NonIsolatedCaller:
return false;

// Parameter isolation is value-dependent and can't be erased in the
// abstract, though. We need to be able to recover the isolation from
// a value.
Expand Down
18 changes: 12 additions & 6 deletions lib/Sema/TypeCheckConcurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7036,15 +7036,19 @@ AnyFunctionType *swift::adjustFunctionTypeForConcurrency(

fnType = applyUnsafeConcurrencyToFunctionType(
fnType, decl, strictChecking, numApplies, isMainDispatchQueue);
Type globalActorType;
std::optional<FunctionTypeIsolation> funcIsolation;
if (decl) {
switch (auto isolation = getActorIsolation(decl)) {
case ActorIsolation::ActorInstance:
// The function type may or may not have parameter isolation.
return fnType;

case ActorIsolation::Nonisolated:
case ActorIsolation::CallerIsolationInheriting:
assert(fnType->getIsolation().isNonIsolated());
funcIsolation = FunctionTypeIsolation::forNonIsolatedCaller();
break;

case ActorIsolation::Nonisolated:
case ActorIsolation::NonisolatedUnsafe:
case ActorIsolation::Unspecified:
assert(fnType->getIsolation().isNonIsolated());
Expand All @@ -7059,20 +7063,22 @@ AnyFunctionType *swift::adjustFunctionTypeForConcurrency(
if (!strictChecking && isolation.preconcurrency())
return fnType;

globalActorType = openType(isolation.getGlobalActor());
Type globalActorType = openType(isolation.getGlobalActor());
funcIsolation = FunctionTypeIsolation::forGlobalActor(globalActorType);
break;
}
}

auto isolation = FunctionTypeIsolation::forGlobalActor(globalActorType);
ASSERT(funcIsolation.has_value());

// If there's no implicit "self" declaration, apply the isolation to
// the outermost function type.
bool hasImplicitSelfDecl = decl && (isa<EnumElementDecl>(decl) ||
(isa<AbstractFunctionDecl>(decl) &&
cast<AbstractFunctionDecl>(decl)->hasImplicitSelfDecl()));
if (!hasImplicitSelfDecl) {
return fnType->withExtInfo(fnType->getExtInfo().withIsolation(isolation));
return fnType->withExtInfo(
fnType->getExtInfo().withIsolation(*funcIsolation));
}

// Dig out the inner function type.
Expand All @@ -7082,7 +7088,7 @@ AnyFunctionType *swift::adjustFunctionTypeForConcurrency(

// Update the inner function type with the isolation.
innerFnType = innerFnType->withExtInfo(
innerFnType->getExtInfo().withIsolation(isolation));
innerFnType->getExtInfo().withIsolation(*funcIsolation));

// Rebuild the outer function type around it.
if (auto genericFnType = dyn_cast<GenericFunctionType>(fnType)) {
Expand Down
54 changes: 30 additions & 24 deletions lib/Sema/TypeCheckType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4203,40 +4203,46 @@ NeverNullType TypeResolver::resolveASTFunctionType(
diag::attr_execution_type_attr_only_on_async);
}

switch (isolation.getKind()) {
case FunctionTypeIsolation::Kind::NonIsolated:
break;
if (executionAttr->getBehavior() == ExecutionKind::Concurrent) {
switch (isolation.getKind()) {
case FunctionTypeIsolation::Kind::NonIsolated:
break;

case FunctionTypeIsolation::Kind::GlobalActor:
diagnoseInvalid(
repr, executionAttr->getAtLoc(),
diag::
attr_execution_concurrent_type_attr_incompatible_with_global_isolation,
isolation.getGlobalActorType());
break;
case FunctionTypeIsolation::Kind::GlobalActor:
diagnoseInvalid(
repr, executionAttr->getAtLoc(),
diag::
attr_execution_concurrent_type_attr_incompatible_with_global_isolation,
isolation.getGlobalActorType());
break;

case FunctionTypeIsolation::Kind::Parameter:
diagnoseInvalid(
repr, executionAttr->getAtLoc(),
diag::
attr_execution_concurrent_type_attr_incompatible_with_isolated_param);
break;
case FunctionTypeIsolation::Kind::Parameter:
diagnoseInvalid(
repr, executionAttr->getAtLoc(),
diag::
attr_execution_concurrent_type_attr_incompatible_with_isolated_param);
break;

case FunctionTypeIsolation::Kind::Erased:
diagnoseInvalid(
repr, executionAttr->getAtLoc(),
diag::
attr_execution_concurrent_type_attr_incompatible_with_isolated_any);
break;
case FunctionTypeIsolation::Kind::Erased:
diagnoseInvalid(
repr, executionAttr->getAtLoc(),
diag::
attr_execution_concurrent_type_attr_incompatible_with_isolated_any);
break;

case FunctionTypeIsolation::Kind::NonIsolatedCaller:
llvm_unreachable("cannot happen because multiple @execution attributes "
"aren't allowed.");
}
}

if (!repr->isInvalid()) {
switch (executionAttr->getBehavior()) {
case ExecutionKind::Concurrent:
// TODO: We need to introduce a new isolation kind to support this.
isolation = FunctionTypeIsolation::forNonIsolated();
break;
case ExecutionKind::Caller:
isolation = FunctionTypeIsolation::forNonIsolated();
isolation = FunctionTypeIsolation::forNonIsolatedCaller();
break;
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/Serialization/Deserialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7133,6 +7133,8 @@ detail::function_deserializer::deserialize(ModuleFile &MF,
auto isolation = swift::FunctionTypeIsolation::forNonIsolated();
if (rawIsolation == unsigned(FunctionTypeIsolation::NonIsolated)) {
// do nothing
} else if (rawIsolation == unsigned(FunctionTypeIsolation::NonIsolatedCaller)) {
isolation = swift::FunctionTypeIsolation::forNonIsolatedCaller();
} else if (rawIsolation == unsigned(FunctionTypeIsolation::Parameter)) {
isolation = swift::FunctionTypeIsolation::forParameter();
} else if (rawIsolation == unsigned(FunctionTypeIsolation::Erased)) {
Expand Down
3 changes: 2 additions & 1 deletion lib/Serialization/ModuleFormat.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const uint16_t SWIFTMODULE_VERSION_MAJOR = 0;
/// describe what change you made. The content of this comment isn't important;
/// it just ensures a conflict if two people change the module format.
/// Don't worry about adhering to the 80-column limit for this line.
const uint16_t SWIFTMODULE_VERSION_MINOR = 921; // remove alloc_vector
const uint16_t SWIFTMODULE_VERSION_MINOR = 922; // function type isolation - caller

/// A standard hash seed used for all string hashes in a serialized module.
///
Expand Down Expand Up @@ -707,6 +707,7 @@ enum class FunctionTypeIsolation : uint8_t {
Parameter,
Erased,
GlobalActorOffset, // Add this to the global actor type ID
NonIsolatedCaller,
};
using FunctionTypeIsolationField = TypeIDField;

Expand Down
2 changes: 2 additions & 0 deletions lib/Serialization/Serialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5781,6 +5781,8 @@ class Serializer::TypeSerializer : public TypeVisitor<TypeSerializer> {
switch (isolation.getKind()) {
case swift::FunctionTypeIsolation::Kind::NonIsolated:
return unsigned(FunctionTypeIsolation::NonIsolated);
case swift::FunctionTypeIsolation::Kind::NonIsolatedCaller:
return unsigned(FunctionTypeIsolation::NonIsolatedCaller);
case swift::FunctionTypeIsolation::Kind::Parameter:
return unsigned(FunctionTypeIsolation::Parameter);
case swift::FunctionTypeIsolation::Kind::Erased:
Expand Down
77 changes: 77 additions & 0 deletions test/Concurrency/attr_execution_conversions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// RUN: %target-typecheck-verify-swift -target %target-swift-5.1-abi-triple

// REQUIRES: concurrency

@execution(concurrent)
func concurrentTest() async {
}

@execution(caller)
func callerTest() async {
}

@MainActor
func actorIsolated() async {}

let _: @execution(caller) () async -> Void = concurrentTest // Ok
let _: @execution(concurrent) () async -> Void = callerTest // Ok

let _: @MainActor () async -> Void = concurrentTest // Ok
let _: @MainActor () async -> Void = callerTest // Ok

let _: @isolated(any) () async -> Void = concurrentTest // Ok
let _: @isolated(any) () async -> Void = callerTest
// expected-error@-1 {{cannot convert value of type '@execution(caller) () async -> ()' to specified type '@isolated(any) () async -> Void'}}

let _: @execution(caller) () async -> Void = actorIsolated // Ok
let _: @execution(concurrent) () async -> Void = actorIsolated // Ok

func testIsolationErasure(fn: @escaping @isolated(any) () async -> Void) {
let _: @execution(concurrent) () async -> Void = fn // Ok
let _: @execution(caller) () async -> Void = fn // Ok
}

func testUpcast(arr: [@execution(caller) () async -> Void]) {
let _: [() async -> Void] = arr // Ok - collection upcast
let _: [String: () async -> Void] = ["": arr]
// expected-error@-1 {{cannot convert value of type '[@execution(caller) () async -> Void]' to expected dictionary value type '() async -> Void'}}
}

// Isolated parameter
func testParameterIsolation(fn: @escaping (isolated (any Actor)?) async -> Void) {
let _: @execution(caller) () async -> Void = fn
// expected-error@-1 {{cannot convert value of type '(isolated (any Actor)?) async -> Void' to specified type '@execution(caller) () async -> Void'}}
let _: @execution(concurrent) () async -> Void = fn
// expected-error@-1 {{cannot convert value of type '(isolated (any Actor)?) async -> Void' to specified type '() async -> Void'}}
}

// Non-conversion situations
do {
struct S<T> {
}
func test<T>(_: S<T>, _: T.Type) {}

test(S<() async -> Void>(), type(of: callerTest))
// expected-error@-1 {{cannot convert value of type '(@execution(caller) () async -> ()).Type' to expected argument type '(() async -> Void).Type'}}

test(S<@execution(caller) () async -> Void>(), type(of: concurrentTest))
// expected-error@-1 {{cannot convert value of type '(() async -> ()).Type' to expected argument type '(@execution(caller) () async -> Void).Type'}}

test(S<@MainActor () async -> Void>(), type(of: callerTest))
// expected-error@-1 {{cannot convert value of type '(@execution(caller) () async -> ()).Type' to expected argument type '(@MainActor () async -> Void).Type'}}

test(S<@MainActor () async -> Void>(), type(of: concurrentTest))
// expected-error@-1 {{cannot convert value of type '(() async -> ()).Type' to expected argument type '(@MainActor () async -> Void).Type'}}

test(S<(isolated (any Actor)?) async -> Void>(), type(of: callerTest))
// expected-error@-1 {{cannot convert value of type '(@execution(caller) () async -> ()).Type' to expected argument type '((isolated (any Actor)?) async -> Void).Type'}}

test(S<(isolated (any Actor)?) async -> Void>(), type(of: concurrentTest))
// expected-error@-1 {{cannot convert value of type '(() async -> ()).Type' to expected argument type '((isolated (any Actor)?) async -> Void).Type'}}

test(S<@isolated(any) () async -> Void>(), type(of: concurrentTest))
// expected-error@-1 {{cannot convert value of type '(() async -> ()).Type' to expected argument type '(@isolated(any) () async -> Void).Type'}}
test(S<@isolated(any) () async -> Void>(), type(of: callerTest))
// expected-error@-1 {{cannot convert value of type '(@execution(caller) () async -> ()).Type' to expected argument type '(@isolated(any) () async -> Void).Type'}}
}

Loading