Skip to content

Commit 5f049b6

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 7e4425c commit 5f049b6

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
@@ -3997,6 +3997,9 @@ ERROR(reasync_without_async_parameter,none,
39973997

39983998
ERROR(inherits_executor_without_async,none,
39993999
"non-async functions cannot inherit an executor", ())
4000+
ERROR(isolation_in_inherits_executor,none,
4001+
"#isolation%select{| (introduced by a default argument)}0 cannot be used "
4002+
"within an '@_unsafeInheritExecutor' function", (bool))
40004003

40014004
ERROR(lifetime_invalid_global_scope,none, "%0 is only valid on methods",
40024005
(DeclAttribute))

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,6 +1999,103 @@ bool swift::isAsyncDecl(ConcreteDeclRef declRef) {
19991999
return false;
20002000
}
20012001

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

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