Skip to content

[Concurrency] Implement an experimental feature for setting default actor isolation per file using a typealias. #80572

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

Closed
wants to merge 2 commits into from
Closed
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 include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -6029,6 +6029,13 @@ ERROR(global_actor_not_usable_from_inline,none,
NOTE(move_global_actor_attr_to_storage_decl,none,
"move global actor attribute to %kind0", (const ValueDecl *))

ERROR(default_isolation_internal,none,
"default isolation can only be set per file",
())
ERROR(default_isolation_custom,none,
"default isolation can only be set to 'MainActor' or 'nonisolated'",
())

ERROR(actor_isolation_multiple_attr_2,none,
"%kind0 has multiple actor-isolation attributes (%1 and %2)",
(const Decl *, DeclAttribute, DeclAttribute))
Expand Down
1 change: 1 addition & 0 deletions include/swift/AST/KnownIdentifiers.def
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ IDENTIFIER(decodeIfPresent)
IDENTIFIER(Decoder)
IDENTIFIER(decoder)
IDENTIFIER(DefaultDistributedActorSystem)
IDENTIFIER(DefaultIsolation)
IDENTIFIER_(Differentiation)
IDENTIFIER_WITH_NAME(PatternMatchVar, "$match")
IDENTIFIER(dynamicallyCall)
Expand Down
1 change: 1 addition & 0 deletions include/swift/AST/KnownSDKTypes.def
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ KNOWN_SDK_TYPE_DECL(ObjectiveC, ObjCBool, StructDecl, 0)
KNOWN_SDK_TYPE_DECL(Concurrency, CheckedContinuation, NominalTypeDecl, 2)
KNOWN_SDK_TYPE_DECL(Concurrency, UnsafeContinuation, NominalTypeDecl, 2)
KNOWN_SDK_TYPE_DECL(Concurrency, MainActor, NominalTypeDecl, 0)
KNOWN_SDK_TYPE_DECL(Concurrency, nonisolated, TypeAliasDecl, 0)
KNOWN_SDK_TYPE_DECL(Concurrency, Job, StructDecl, 0) // legacy type; prefer ExecutorJob
KNOWN_SDK_TYPE_DECL(Concurrency, ExecutorJob, StructDecl, 0)
KNOWN_SDK_TYPE_DECL(Concurrency, UnownedJob, StructDecl, 0)
Expand Down
7 changes: 4 additions & 3 deletions include/swift/AST/SourceFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "swift/AST/Import.h"
#include "swift/AST/SynthesizedFileUnit.h"
#include "swift/Basic/Debug.h"
#include "swift/Basic/LangOptions.h"
#include "llvm/ADT/Hashing.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SetVector.h"
Expand Down Expand Up @@ -58,9 +59,9 @@ using ImportAccessLevel = std::optional<AttributedImport<ImportedModule>>;
///
/// Vended by SourceFile::getLanguageOptions().
struct SourceFileLangOptions {
/// If unset, no value was provided. If a Type, that type is the type of the
/// isolation. If set to an empty type, nil was specified explicitly.
std::optional<Type> defaultIsolation;
/// The default actor isolation to infer on declarations
/// within the source file.
std::optional<DefaultIsolation> defaultIsolation;
};

/// A file containing Swift source code.
Expand Down
20 changes: 20 additions & 0 deletions include/swift/AST/TypeCheckRequests.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "swift/AST/SourceFile.h"
#include "swift/AST/Type.h"
#include "swift/AST/TypeResolutionStage.h"
#include "swift/Basic/LangOptions.h"
#include "swift/Basic/Statistic.h"
#include "swift/Basic/TaggedUnion.h"
#include "swift/Basic/TypeID.h"
Expand Down Expand Up @@ -1621,6 +1622,25 @@ class ActorIsolationRequest :
bool isCached() const { return true; }
};

/// Determine the default actor isolation for the given source file.
class DefaultActorIsolationRequest :
public SimpleRequest<DefaultActorIsolationRequest,
std::optional<DefaultIsolation>(SourceFile *),
RequestFlags::Cached> {
public:
using SimpleRequest::SimpleRequest;

private:
friend SimpleRequest;

std::optional<DefaultIsolation>
evaluate(Evaluator &evaluator, SourceFile *file) const;

public:
// Caching
bool isCached() const { return true; }
};

/// Determine whether the given function should have an isolated 'self'.
class HasIsolatedSelfRequest :
public SimpleRequest<HasIsolatedSelfRequest,
Expand Down
3 changes: 3 additions & 0 deletions include/swift/AST/TypeCheckerTypeIDZone.def
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ SWIFT_REQUEST(TypeChecker, GlobalActorAttributeRequest,
SWIFT_REQUEST(TypeChecker, ActorIsolationRequest,
ActorIsolationState(ValueDecl *),
Cached, NoLocationInfo)
SWIFT_REQUEST(TypeChecker, DefaultActorIsolationRequest,
std::optional<DefaultIsolation>(SourceFile *),
Cached, NoLocationInfo)
SWIFT_REQUEST(TypeChecker, HasIsolatedSelfRequest,
bool(ValueDecl *),
Uncached, NoLocationInfo)
Expand Down
3 changes: 3 additions & 0 deletions include/swift/Basic/Features.def
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,9 @@ EXPERIMENTAL_FEATURE(InferIsolatedConformances, true)
/// Allow SwiftSettings
EXPERIMENTAL_FEATURE(SwiftSettings, false)

/// Enable setting default isolation per file via typealias.
EXPERIMENTAL_FEATURE(DefaultIsolationTypealias, true)

/// Syntax sugar features for concurrency.
EXPERIMENTAL_FEATURE(ConcurrencySyntaxSugar, true)

Expand Down
1 change: 1 addition & 0 deletions lib/AST/FeatureSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ UNINTERESTING_FEATURE(ImportNonPublicCxxMembers)
UNINTERESTING_FEATURE(CXXForeignReferenceTypeInitializers)
UNINTERESTING_FEATURE(CoroutineAccessorsUnwindOnCallerError)
UNINTERESTING_FEATURE(AllowRuntimeSymbolDeclarations)
UNINTERESTING_FEATURE(DefaultIsolationTypealias)

static bool usesFeatureSwiftSettings(const Decl *decl) {
// We just need to guard `#SwiftSettings`.
Expand Down
29 changes: 14 additions & 15 deletions lib/AST/Module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4223,12 +4223,11 @@ struct SwiftSettingsWalker : ASTWalker {
}

/// Given a specific CallExpr, pattern matches the CallExpr's first argument
/// to validate it is MainActor.self. Returns CanType() if the CallExpr has
/// multiple parameters or if its first parameter is not a MainActor.self.
/// to validate it is MainActor.self.
///
/// This is used when pattern matching against
/// .defaultIsolation(MainActor.self).
CanType patternMatchDefaultIsolationMainActor(CallExpr *callExpr);
/// \returns \c true if the call has one argument that matches
/// \c MainActor.self, and \c false otherwise.
bool patternMatchDefaultIsolationMainActor(CallExpr *callExpr);

/// Validates that macroExpr is a well formed "SwiftSettings" macro. Returns
/// true if we can process it and false otherwise.
Expand Down Expand Up @@ -4294,16 +4293,16 @@ struct SwiftSettingsWalker : ASTWalker {
}

// Otherwise, set things up appropriately.
if (auto actor = patternMatchDefaultIsolationMainActor(callExpr)) {
if (patternMatchDefaultIsolationMainActor(callExpr)) {
expr = callExpr;
result.defaultIsolation = actor;
result.defaultIsolation = DefaultIsolation::MainActor;
foundValidArg = true;
continue;
}

if (isa<NilLiteralExpr>(callExpr->getArgs()->getExpr(0))) {
expr = callExpr;
result.defaultIsolation = {Type()};
result.defaultIsolation = DefaultIsolation::Nonisolated;
foundValidArg = true;
continue;
}
Expand Down Expand Up @@ -4405,18 +4404,18 @@ SwiftSettingsWalker::getSwiftSettingArgDecl(Argument arg) {
return {{callExpr, f}};
}

CanType
bool
SwiftSettingsWalker::patternMatchDefaultIsolationMainActor(CallExpr *callExpr) {
// Grab the dot self expr.
auto *selfExpr = dyn_cast<DotSelfExpr>(callExpr->getArgs()->getExpr(0));
if (!selfExpr)
return CanType();
return false;

// Then validate we have something that is MainActor.
auto *declRefExpr = dyn_cast<UnresolvedDeclRefExpr>(selfExpr->getSubExpr());
if (!declRefExpr ||
!declRefExpr->getName().getBaseName().getIdentifier().is("MainActor"))
return CanType();
return false;

// Then use unqualified lookup descriptor to find our MainActor.
UnqualifiedLookupDescriptor lookupDesc{
Expand All @@ -4425,20 +4424,20 @@ SwiftSettingsWalker::patternMatchDefaultIsolationMainActor(CallExpr *callExpr) {
auto lookup = evaluateOrDefault(ctx.evaluator,
UnqualifiedLookupRequest{lookupDesc}, {});
if (lookup.allResults().empty())
return CanType();
return false;

// Then grab our nominal type decl and make sure it is from the concurrency
// module.
auto *nomDecl =
dyn_cast<NominalTypeDecl>(lookup.allResults().front().getValueDecl());
if (!nomDecl)
return CanType();
return false;
auto *nomDeclDC = nomDecl->getDeclContext();
auto *nomDeclModule = nomDecl->getParentModule();
if (!nomDeclDC->isModuleScopeContext() || !nomDeclModule->isConcurrencyModule())
return CanType();
return false;

return nomDecl->getDeclaredType()->getCanonicalType();
return true;
}

SourceFileLangOptions
Expand Down
119 changes: 83 additions & 36 deletions lib/Sema/TypeCheckConcurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5732,19 +5732,93 @@ static void addAttributesForActorIsolation(ValueDecl *value,
}
}

std::optional<DefaultIsolation>
DefaultActorIsolationRequest::evaluate(Evaluator &evaluator,
SourceFile *sourceFile) const {
if (!sourceFile)
return std::nullopt;

auto &ctx = sourceFile->getASTContext();
auto defaultModuleIsolaton = ctx.LangOpts.DefaultIsolationBehavior;
auto mainActorType = ctx.getMainActorType();
if (!mainActorType)
return defaultModuleIsolaton;

if (ctx.LangOpts.hasFeature(Feature::SwiftSettings)) {
auto options = sourceFile->getLanguageOptions();
if (auto isolation = options.defaultIsolation) {
return *isolation;
}
}

if (ctx.LangOpts.hasFeature(Feature::DefaultIsolationTypealias)) {
auto nonisolatedType = ctx.getnonisolatedType();

auto decls = sourceFile->getTopLevelDecls();
if (decls.empty())
return defaultModuleIsolaton;

auto locInFile = decls.front()->getStartLoc();
auto defaultIsolationResult = TypeChecker::lookupUnqualified(
sourceFile->getModuleScopeContext(),
DeclNameRef(ctx.Id_DefaultIsolation),
locInFile);
for (auto found : defaultIsolationResult) {
auto *decl = found.getValueDecl();
if (!decl)
continue;

auto *typealias = dyn_cast<TypeAliasDecl>(decl);
if (!typealias ||
!typealias->getDeclContext()->isModuleScopeContext())
continue;

// We have a top-level 'DefaultIsolation' typealias. We can assume
// it's the only one, because multiple top-level typealiases with
// the same name is a redeclaration error, and default isolation
// cannot be set by an imported library.

// The typealias can only be used to set default isolation per file.
if (typealias->getFormalAccess() >= AccessLevel::Internal) {
typealias->diagnose(diag::default_isolation_internal);
break;
}

auto type = typealias->getUnderlyingType();
if (type->isEqual(mainActorType))
return DefaultIsolation::MainActor;

if (type->isEqual(nonisolatedType))
return DefaultIsolation::Nonisolated;

// The underlying type of the typealias must either be 'MainActor'
// or 'nonisolated'.
typealias->diagnose(diag::default_isolation_custom);
break;
}
}

return defaultModuleIsolaton;
}

/// Determine the default isolation and isolation source for this declaration,
/// which may still be overridden by other inference rules.
static std::tuple<InferredActorIsolation, ValueDecl *,
std::optional<ActorIsolation>>
computeDefaultInferredActorIsolation(ValueDecl *value) {
auto &ctx = value->getASTContext();

// If we are supposed to infer main actor isolation by default for entities
// within our module, make our default isolation main actor.
if (value->getModuleContext() == ctx.MainModule) {
auto globalActorHelper = [&](Type globalActor)
-> std::optional<std::tuple<InferredActorIsolation, ValueDecl *,
std::optional<ActorIsolation>>> {
// Determine whether default isolation is set to MainActor, either for
// the entire module or in this specific file.
auto mainActorType = ctx.getMainActorType();
if (mainActorType && value->getModuleContext() == ctx.MainModule) {
// See if we have one specified by our file unit.
auto *sourceFile = value->getDeclContext()->getParentSourceFile();
auto defaultIsolation = evaluateOrDefault(
ctx.evaluator, DefaultActorIsolationRequest{sourceFile},
ctx.LangOpts.DefaultIsolationBehavior);

if (defaultIsolation == DefaultIsolation::MainActor) {
// Default global actor isolation does not apply to any declarations
// within actors and distributed actors.
bool inActorContext = false;
Expand All @@ -5761,39 +5835,12 @@ computeDefaultInferredActorIsolation(ValueDecl *value) {
if (isa<TypeDecl>(value) || isa<ExtensionDecl>(value) ||
isa<AbstractStorageDecl>(value) || isa<FuncDecl>(value) ||
isa<ConstructorDecl>(value)) {
return {
{{ActorIsolation::forGlobalActor(globalActor), {}}, nullptr, {}}};
}
}

return {};
};

// Otherwise, see if we have one specified by our file unit.
bool ignoreUnspecifiedMeansMainActorIsolated = false;
if (ctx.LangOpts.hasFeature(Feature::SwiftSettings)) {
if (auto *sourceFile = value->getDeclContext()->getParentSourceFile()) {
auto options = sourceFile->getLanguageOptions();
if (auto isolation = options.defaultIsolation) {
if (*isolation) {
auto result = globalActorHelper(*options.defaultIsolation);
if (result)
return *result;
} else {
// If we found a nil type, then we know we should ignore unspecified
// means main actor isolated.
ignoreUnspecifiedMeansMainActorIsolated = true;
}
auto mainActorIsolation =
ActorIsolation::forGlobalActor(mainActorType);
return {{mainActorIsolation, {}}, nullptr, {}};
}
}
}

// If we are required to use main actor... just use that.
if (!ignoreUnspecifiedMeansMainActorIsolated &&
ctx.LangOpts.DefaultIsolationBehavior == DefaultIsolation::MainActor)
if (auto result =
globalActorHelper(ctx.getMainActorType()->mapTypeOutOfContext()))
return *result;
}

// If we have an async function... by default we inherit isolation.
Expand Down
2 changes: 2 additions & 0 deletions stdlib/public/Concurrency/Actor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,5 @@ public func extractIsolation<each Arg, Result>(
return Builtin.extractFunctionIsolation(fn)
}
#endif

public typealias nonisolated = Never
44 changes: 44 additions & 0 deletions test/Concurrency/default_isolation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// RUN: %empty-directory(%t)
// RUN: split-file %s %t

// REQUIRES: concurrency
// REQUIRES: swift_feature_DefaultIsolationTypealias

// RUN: %target-swift-frontend -enable-experimental-feature DefaultIsolationTypealias -emit-sil -swift-version 6 -disable-availability-checking %t/main.swift %t/concurrent.swift | %FileCheck %s

//--- main.swift

private typealias DefaultIsolation = MainActor

class C {
// CHECK: // static C.shared.getter
// CHECK-NEXT: // Isolation: global_actor. type: MainActor
static let shared = C()

// CHECK: // C.init()
// CHECK-NEXT: // Isolation: global_actor. type: MainActor
init() {}
}

// CHECK: // test()
// CHECK-NEXT: // Isolation: global_actor. type: MainActor
func test() {
// CHECK: // closure #1 in test()
// CHECK-NEXT: // Isolation: nonisolated
Task.detached {
let s = S(value: 0)
}
}


//--- concurrent.swift

private typealias DefaultIsolation = nonisolated

// CHECK: // S.init(value:)
// CHECK-NEXT: // Isolation: unspecified
struct S {
// CHECK: // S.value.getter
// CHECK-NEXT: // Isolation: unspecified
var value: Int
}
Loading