Skip to content

Commit 40128f3

Browse files
committed
Diagnose attempts to use #isolation within an @_unsafeInheritExecutor function
An `@_unsafeInheritExecutor` function is unsafe because it doesn't really "inherit" the executor, it just avoids immediately hopping off the executor. That means that using `#isolation` within such a function is fundamentally broken. Ban the use of `#isolation` within such a function, providing a Fix-It that removes the `@_unsafeInheritExecutor` attribute and adds a defaulted parameter isolation: (any Actor)? = #isolation instead. That's the real, safe pattern that want going forward. We did say it was unsafe, after all. Part of rdar://131151376.
1 parent 03a9292 commit 40128f3

File tree

3 files changed

+147
-0
lines changed

3 files changed

+147
-0
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3977,6 +3977,9 @@ ERROR(reasync_without_async_parameter,none,
39773977

39783978
ERROR(inherits_executor_without_async,none,
39793979
"non-async functions cannot inherit an executor", ())
3980+
ERROR(isolation_in_inherits_executor,none,
3981+
"#isolation%select{| (introduced by a default argument)}0 cannot be used "
3982+
"within an '@_unsafeInheritExecutor' function", (bool))
39803983

39813984
ERROR(lifetime_invalid_global_scope,none, "%0 is only valid on methods",
39823985
(DeclAttribute))

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,103 @@ bool swift::isAsyncDecl(ConcreteDeclRef declRef) {
20032003
return false;
20042004
}
20052005

2006+
/// Find an enclosing function that has @
2007+
static AbstractFunctionDecl *enclosingUnsafeInheritsExecutor(
2008+
const DeclContext *dc) {
2009+
for (; dc; dc = dc->getParent()) {
2010+
if (auto func = dyn_cast<AbstractFunctionDecl>(dc)) {
2011+
if (func->getAttrs().hasAttribute<UnsafeInheritExecutorAttr>()) {
2012+
return const_cast<AbstractFunctionDecl *>(func);
2013+
}
2014+
2015+
return nullptr;
2016+
}
2017+
2018+
if (isa<AbstractClosureExpr>(dc))
2019+
return nullptr;
2020+
2021+
if (dc->isTypeContext())
2022+
return nullptr;
2023+
}
2024+
2025+
return nullptr;
2026+
}
2027+
2028+
/// Adjust the location used for diagnostics about #isolation to account for
2029+
/// the fact that they show up in macro expansions.
2030+
///
2031+
/// Returns a pair containing the updated location and whether it's part of
2032+
/// a default argument.
2033+
static std::pair<SourceLoc, bool> adjustPoundIsolationDiagLoc(
2034+
CurrentContextIsolationExpr *isolationExpr,
2035+
ModuleDecl *module
2036+
) {
2037+
// Not part of a macro expansion.
2038+
SourceLoc diagLoc = isolationExpr->getLoc();
2039+
auto sourceFile = module->getSourceFileContainingLocation(diagLoc);
2040+
if (!sourceFile)
2041+
return { diagLoc, false };
2042+
auto macroExpansionRange = sourceFile->getMacroInsertionRange();
2043+
if (macroExpansionRange.Start.isInvalid())
2044+
return { diagLoc, false };
2045+
2046+
diagLoc = macroExpansionRange.Start;
2047+
2048+
// If this is from a default argument, note that and go one more
2049+
// level "out" to the place where the default argument was
2050+
// introduced.
2051+
auto expansionSourceFile = module->getSourceFileContainingLocation(diagLoc);
2052+
if (!expansionSourceFile ||
2053+
expansionSourceFile->Kind != SourceFileKind::DefaultArgument)
2054+
return { diagLoc, false };
2055+
2056+
return {
2057+
expansionSourceFile->getNodeInEnclosingSourceFile().getStartLoc(),
2058+
true
2059+
};
2060+
}
2061+
2062+
/// Replace the @_unsafeInheritExecutor with a defaulted isolation
2063+
/// parameter.
2064+
static void replaceUnsafeInheritExecutorWithDefaultedIsolationParam(
2065+
AbstractFunctionDecl *func, InFlightDiagnostic &diag) {
2066+
auto attr = func->getAttrs().getAttribute<UnsafeInheritExecutorAttr>();
2067+
assert(attr && "Caller didn't validate the presence of the attribute");
2068+
2069+
// Look for the place where we should insert the new 'isolation' parameter.
2070+
// We insert toward the back, but skip over any parameters that have function
2071+
// type.
2072+
unsigned insertionPos = func->getParameters()->size();
2073+
while (insertionPos > 0) {
2074+
Type paramType = func->getParameters()->get(insertionPos - 1)->getInterfaceType();
2075+
if (paramType->lookThroughSingleOptionalType()->is<AnyFunctionType>()) {
2076+
--insertionPos;
2077+
continue;
2078+
}
2079+
2080+
break;
2081+
}
2082+
2083+
// Determine the text to insert. We put the commas before and after, then
2084+
// slice them away depending on whether we have parameters before or after.
2085+
StringRef newParameterText = ", isolation: isolated (any Actor)? = #isolation, ";
2086+
if (insertionPos == 0)
2087+
newParameterText = newParameterText.drop_front(2);
2088+
if (insertionPos == func->getParameters()->size())
2089+
newParameterText = newParameterText.drop_back(2);
2090+
2091+
// Determine where to insert the new parameter.
2092+
SourceLoc insertionLoc;
2093+
if (insertionPos < func->getParameters()->size()) {
2094+
insertionLoc = func->getParameters()->get(insertionPos)->getStartLoc();
2095+
} else {
2096+
insertionLoc = func->getParameters()->getRParenLoc();
2097+
}
2098+
2099+
diag.fixItRemove(attr->getRangeWithAt());
2100+
diag.fixItInsert(insertionLoc, newParameterText);
2101+
}
2102+
20062103
/// Check if it is safe for the \c globalActor qualifier to be removed from
20072104
/// \c ty, when the function value of that type is isolated to that actor.
20082105
///
@@ -3742,6 +3839,25 @@ namespace {
37423839

37433840
void recordCurrentContextIsolation(
37443841
CurrentContextIsolationExpr *isolationExpr) {
3842+
// #isolation does not work within an `@_unsafeInheritExecutor` function.
3843+
if (auto func = enclosingUnsafeInheritsExecutor(getDeclContext())) {
3844+
// This expression is always written as a macro #isolation in source,
3845+
// so find the enclosing macro expansion expression's location.
3846+
SourceLoc diagLoc;
3847+
bool inDefaultArgument;
3848+
std::tie(diagLoc, inDefaultArgument) = adjustPoundIsolationDiagLoc(
3849+
isolationExpr, getDeclContext()->getParentModule());
3850+
3851+
bool inConcurrencyModule = getDeclContext()->getParentModule()->getName()
3852+
.str().equals("_Concurrency");
3853+
3854+
auto diag = ctx.Diags.diagnose(diagLoc,
3855+
diag::isolation_in_inherits_executor,
3856+
inDefaultArgument);
3857+
diag.limitBehaviorIf(inConcurrencyModule, DiagnosticBehavior::Warning);
3858+
replaceUnsafeInheritExecutorWithDefaultedIsolationParam(func, diag);
3859+
}
3860+
37453861
// If an actor has already been assigned, we're done.
37463862
if (isolationExpr->getActor())
37473863
return;

test/Concurrency/unsafe_inherit_executor.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,31 @@ actor MyActor {
3939
await useNonSendable(object: self.object)
4040
}
4141
}
42+
43+
// Note: the tests below are line-number-sensitive.
44+
func inheritsIsolationProperly(isolation: isolated (any Actor)? = #isolation) async { }
45+
46+
// @_unsafeInheritExecutor does not work with #isolation
47+
@_unsafeInheritExecutor
48+
func unsafeCallerA(x: Int) async {
49+
await inheritsIsolationProperly()
50+
// expected-error@-1{{#isolation (introduced by a default argument) cannot be used within an '@_unsafeInheritExecutor' function}}{{47:1-24=}}{{48:26-26=, isolation: isolated (any Actor)? = #isolation}}
51+
}
52+
53+
@_unsafeInheritExecutor
54+
func unsafeCallerB() async {
55+
await inheritsIsolationProperly(isolation: #isolation)
56+
// expected-error@-1{{#isolation cannot be used within an '@_unsafeInheritExecutor' function}}{{53:1-24=}}{{54:20-20=isolation: isolated (any Actor)? = #isolation}}
57+
}
58+
59+
@_unsafeInheritExecutor
60+
func unsafeCallerC(x: Int, fn: () -> Void, fn2: () -> Void) async {
61+
await inheritsIsolationProperly()
62+
// expected-error@-1{{#isolation (introduced by a default argument) cannot be used within an '@_unsafeInheritExecutor' function}}{{59:1-24=}}{{60:28-28=, isolation: isolated (any Actor)? = #isolation, }}
63+
}
64+
65+
@_unsafeInheritExecutor
66+
func unsafeCallerB(x: some AsyncSequence<Int, Never>) async {
67+
for await _ in x { }
68+
// expected-error@-1 2{{#isolation cannot be used within an `@_unsafeInheritExecutor` function}}
69+
}

0 commit comments

Comments
 (0)