Skip to content

[concurrency] Perform some fixes so that transfernonsendable_closureliterals_isolationinference.swift now passes. #82858

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
Jul 8, 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
8 changes: 8 additions & 0 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -8702,6 +8702,14 @@ ERROR(inherit_actor_context_only_on_async_or_isolation_erased_params,none,
"asynchronous function types",
(DeclAttribute))

ERROR(inherit_actor_context_with_concurrent,none,
"'@_inheritActorContext' attribute cannot be used together with %0",
(const TypeAttribute *))

ERROR(inherit_actor_context_with_nonisolated_nonsending,none,
"'@_inheritActorContext' attribute cannot be used together with %0",
(TypeRepr *))

//===----------------------------------------------------------------------===//
// MARK: @concurrent and nonisolated(nonsending) attributes
//===----------------------------------------------------------------------===//
Expand Down
64 changes: 64 additions & 0 deletions lib/SILGen/SILGenExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,10 @@ namespace {
RValue
emitFunctionCvtFromExecutionCallerToGlobalActor(FunctionConversionExpr *E,
SGFContext C);

RValue emitFunctionCvtForNonisolatedNonsendingClosureExpr(
FunctionConversionExpr *E, SGFContext C);

RValue visitActorIsolationErasureExpr(ActorIsolationErasureExpr *E,
SGFContext C);
RValue visitExtractFunctionIsolationExpr(ExtractFunctionIsolationExpr *E,
Expand Down Expand Up @@ -2031,6 +2035,44 @@ RValueEmitter::emitFunctionCvtToExecutionCaller(FunctionConversionExpr *e,
return RValue(SGF, e, destType, result);
}

RValue RValueEmitter::emitFunctionCvtForNonisolatedNonsendingClosureExpr(
FunctionConversionExpr *E, SGFContext C) {
// The specific AST pattern for this looks as follows:
//
// (function_conversion_expr type="nonisolated(nonsending) () async -> Void"
// (closure_expr type="() async -> ()" isolated_to_caller_isolation))
CanAnyFunctionType destType =
cast<FunctionType>(E->getType()->getCanonicalType());
auto subExpr = E->getSubExpr()->getSemanticsProvidingExpr();

// If we do not have a closure or if that closure is not caller isolation
// inheriting, bail.
auto *closureExpr = dyn_cast<ClosureExpr>(subExpr);
if (!closureExpr ||
!closureExpr->getActorIsolation().isCallerIsolationInheriting())
return RValue();

// Then grab our closure type... make sure it is non isolated and then make
// sure it is the same as our destType but with nonisolated.
CanAnyFunctionType closureType =
cast<FunctionType>(closureExpr->getType()->getCanonicalType());
if (!closureType->getIsolation().isNonIsolated() ||
closureType !=
destType->withIsolation(FunctionTypeIsolation::forNonIsolated())
->getCanonicalType())
return RValue();

// NOTE: This is a partial inline of getClosureTypeInfo. We do this so we have
// more control and make this change less viral in the compiler for 6.2.
auto newExtInfo = closureType->getExtInfo().withIsolation(
FunctionTypeIsolation::forNonIsolatedCaller());
closureType = closureType.withExtInfo(newExtInfo);
auto info = SGF.getFunctionTypeInfo(closureType);

auto closure = emitClosureReference(closureExpr, info);
return RValue(SGF, closureExpr, destType, closure);
}

RValue RValueEmitter::emitFunctionCvtFromExecutionCallerToGlobalActor(
FunctionConversionExpr *e, SGFContext C) {
// We are pattern matching a conversion sequence like the following:
Expand Down Expand Up @@ -2142,6 +2184,28 @@ RValue RValueEmitter::visitFunctionConversionExpr(FunctionConversionExpr *e,
// TODO: Move this up when we can emit closures directly with C calling
// convention.
auto subExpr = e->getSubExpr()->getSemanticsProvidingExpr();

// Before we go any further into emitting the convert function expr, see if
// our SubExpr is a ClosureExpr with the exact same type as our
// FunctionConversionExpr except with the FunctionConversionExpr adding
// nonisolated(nonsending). Then see if the ClosureExpr itself (even though it
// is not nonisolated(nonsending) typed is considered to have
// nonisolated(nonsending) isolation. In such a case, emit the closure
// directly. We are going to handle it especially in closure emission to work
// around the missing information in the type.
//
// DISCUSSION: We need to do this here since in the Expression TypeChecker we
// do not have access to capture information when we would normally want to
// mark the closure type as being nonisolated(nonsending). As a result, we
// cannot know if the nonisolated(nonsending) should be overridden by for
// example an actor that is captured by the closure. So to work around this in
// Sema, we still mark the ClosureExpr as having the appropriate isolation
// even though its type does not have it... and then we work around this here
// and also in getClosureTypeInfo.
if (destType->getIsolation().isNonIsolatedCaller())
if (auto rv = emitFunctionCvtForNonisolatedNonsendingClosureExpr(e, C))
return rv;

// Look through `as` type ascriptions that don't induce bridging too.
while (auto subCoerce = dyn_cast<CoerceExpr>(subExpr)) {
// Coercions that introduce bridging aren't simple type ascriptions.
Expand Down
17 changes: 0 additions & 17 deletions lib/Sema/CSApply.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7849,23 +7849,6 @@ Expr *ExprRewriter::coerceToType(Expr *expr, Type toType,
}
}

// If we have a ClosureExpr, then we can safely propagate the
// 'nonisolated(nonsending)' isolation if it's not explicitly
// marked as `@concurrent`.
if (toEI.getIsolation().isNonIsolatedCaller() &&
(fromEI.getIsolation().isNonIsolated() &&
!isClosureMarkedAsConcurrent(expr))) {
auto newFromFuncType = fromFunc->withIsolation(
FunctionTypeIsolation::forNonIsolatedCaller());
if (applyTypeToClosureExpr(cs, expr, newFromFuncType)) {
fromFunc = newFromFuncType->castTo<FunctionType>();
// Propagating 'nonisolated(nonsending)' might have satisfied the entire
// conversion. If so, we're done, otherwise keep converting.
if (fromFunc->isEqual(toType))
return expr;
}
}

if (ctx.LangOpts.isDynamicActorIsolationCheckingEnabled()) {
// Passing a synchronous global actor-isolated function value and
// parameter that expects a synchronous non-isolated function type could
Expand Down
3 changes: 3 additions & 0 deletions lib/Sema/TypeCheckAttr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7871,6 +7871,9 @@ void AttributeChecker::visitInheritActorContextAttr(
return;

auto paramTy = P->getInterfaceType();
if (paramTy->hasError())
return;

auto *funcTy =
paramTy->lookThroughAllOptionalTypes()->getAs<AnyFunctionType>();
if (!funcTy) {
Expand Down
29 changes: 27 additions & 2 deletions lib/Sema/TypeCheckConcurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4964,8 +4964,33 @@ ActorIsolation ActorIsolationChecker::determineClosureIsolation(
closure->getParent(), getClosureActorIsolation);
preconcurrency |= parentIsolation.preconcurrency();

return computeClosureIsolationFromParent(closure, parentIsolation,
checkIsolatedCapture);
auto normalIsolation = computeClosureIsolationFromParent(
closure, parentIsolation, checkIsolatedCapture);

// The solver has to be conservative and produce a conversion to
// `nonisolated(nonsending)` because at solution application time
// we don't yet know whether there are any captures which would
// make closure isolated.
//
// At this point we know that closure is not explicitly annotated with
// global actor, nonisolated/@concurrent attributes and doesn't have
// isolated parameters. If our closure is nonisolated and we have a
// conversion to nonisolated(nonsending), then we should respect that.
if (auto *explicitClosure = dyn_cast<ClosureExpr>(closure);
!normalIsolation.isGlobalActor()) {
if (auto *fce =
dyn_cast_or_null<FunctionConversionExpr>(Parent.getAsExpr())) {
auto expectedIsolation =
fce->getType()->castTo<FunctionType>()->getIsolation();
if (expectedIsolation.isNonIsolatedCaller()) {
auto captureInfo = explicitClosure->getCaptureInfo();
if (!captureInfo.getIsolatedParamCapture())
return ActorIsolation::forCallerIsolationInheriting();
}
}
}

return normalIsolation;
}();

// Apply computed preconcurrency.
Expand Down
3 changes: 3 additions & 0 deletions lib/Sema/TypeCheckDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2337,6 +2337,9 @@ static Type validateParameterType(ParamDecl *decl) {
if (dc->isInSpecializeExtensionContext())
options |= TypeResolutionFlags::AllowUsableFromInline;

if (decl->getAttrs().hasAttribute<InheritActorContextAttr>())
options |= TypeResolutionFlags::InheritsActorContext;

Type Ty;

auto *nestedRepr = decl->getTypeRepr();
Expand Down
37 changes: 25 additions & 12 deletions lib/Sema/TypeCheckType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4219,6 +4219,11 @@ NeverNullType TypeResolver::resolveASTFunctionType(
attr->getAttrName());
}

if (options.contains(TypeResolutionFlags::InheritsActorContext)) {
diagnoseInvalid(repr, attr->getAttrLoc(),
diag::inherit_actor_context_with_concurrent, attr);
}

switch (isolation.getKind()) {
case FunctionTypeIsolation::Kind::NonIsolated:
break;
Expand Down Expand Up @@ -4265,18 +4270,20 @@ NeverNullType TypeResolver::resolveASTFunctionType(
if (!repr->isInvalid())
isolation = FunctionTypeIsolation::forNonIsolated();
} else if (!getWithoutClaiming<CallerIsolatedTypeRepr>(attrs)) {
// Infer async function type as `nonisolated(nonsending)` if there is
// no `@concurrent` or `nonisolated(nonsending)` attribute and isolation
// is nonisolated.
if (ctx.LangOpts.hasFeature(Feature::NonisolatedNonsendingByDefault) &&
repr->isAsync() && isolation.isNonIsolated()) {
isolation = FunctionTypeIsolation::forNonIsolatedCaller();
} else if (ctx.LangOpts
.getFeatureState(Feature::NonisolatedNonsendingByDefault)
.isEnabledForMigration()) {
// Diagnose only in the interface stage, which is run once.
if (inStage(TypeResolutionStage::Interface)) {
warnAboutNewNonisolatedAsyncExecutionBehavior(ctx, repr, isolation);
if (!options.contains(TypeResolutionFlags::InheritsActorContext)) {
// Infer async function type as `nonisolated(nonsending)` if there is
// no `@concurrent` or `nonisolated(nonsending)` attribute and isolation
// is nonisolated.
if (ctx.LangOpts.hasFeature(Feature::NonisolatedNonsendingByDefault) &&
repr->isAsync() && isolation.isNonIsolated()) {
isolation = FunctionTypeIsolation::forNonIsolatedCaller();
} else if (ctx.LangOpts
.getFeatureState(Feature::NonisolatedNonsendingByDefault)
.isEnabledForMigration()) {
// Diagnose only in the interface stage, which is run once.
if (inStage(TypeResolutionStage::Interface)) {
warnAboutNewNonisolatedAsyncExecutionBehavior(ctx, repr, isolation);
}
}
}
}
Expand Down Expand Up @@ -5326,6 +5333,12 @@ TypeResolver::resolveCallerIsolatedTypeRepr(CallerIsolatedTypeRepr *repr,
return ErrorType::get(getASTContext());
}

if (options.contains(TypeResolutionFlags::InheritsActorContext)) {
diagnoseInvalid(repr, repr->getLoc(),
diag::inherit_actor_context_with_nonisolated_nonsending,
repr);
}

if (!fnType->isAsync()) {
diagnoseInvalid(repr, repr->getStartLoc(),
diag::nonisolated_nonsending_only_on_async, repr);
Expand Down
3 changes: 3 additions & 0 deletions lib/Sema/TypeCheckType.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ enum class TypeResolutionFlags : uint16_t {

/// Whether the immediate context has an @escaping attribute.
DirectEscaping = 1 << 14,

/// We are in a `@_inheritActorContext` parameter declaration.
InheritsActorContext = 1 << 15,
};

/// Type resolution contexts that require special handling.
Expand Down
19 changes: 16 additions & 3 deletions test/Concurrency/attr_execution/attr_execution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func callerTest() async {}
struct Test {
// CHECK-LABEL: // closure #1 in variable initialization expression of Test.x
// CHECK: // Isolation: caller_isolation_inheriting
// CHECK: sil private [ossa] @$s14attr_execution4TestV1xyyYaYCcvpfiyyYaYCcfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
// CHECK: sil private [ossa] @$s14attr_execution4TestV1xyyYaYCcvpfiyyYacfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
var x: () async -> Void = {}

// CHECK-LABEL: // Test.test()
Expand Down Expand Up @@ -67,16 +67,29 @@ func takesClosure(fn: () async -> Void) {
}

// CHECK-LABEL: sil hidden [ossa] @$s14attr_execution11testClosureyyF : $@convention(thin) () -> ()
// CHECK: [[CLOSURE:%.*]] = function_ref @$s14attr_execution11testClosureyyFyyYaYCXEfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
// CHECK: [[CLOSURE:%.*]] = function_ref @$s14attr_execution11testClosureyyFyyYaXEfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
// CHECK: [[THUNKED_CLOSURE:%.*]] = thin_to_thick_function %0 to $@noescape @async @callee_guaranteed (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
// CHECK: [[TAKES_CLOSURE:%.*]] = function_ref @$s14attr_execution12takesClosure2fnyyyYaYCXE_tF : $@convention(thin) (@guaranteed @noescape @async @callee_guaranteed (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()) -> ()
// CHECK: apply [[TAKES_CLOSURE]]([[THUNKED_CLOSURE]])
// CHECK: } // end sil function '$s14attr_execution11testClosureyyF'

// CHECK-LABEL: // closure #1 in testClosure()
// CHECK: // Isolation: caller_isolation_inheriting
// CHECK: sil private [ossa] @$s14attr_execution11testClosureyyFyyYaYCXEfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
// CHECK: sil private [ossa] @$s14attr_execution11testClosureyyFyyYaXEfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
func testClosure() {
takesClosure {
}
}

// CHECK-LABEL: // testInheritsActor(fn:)
// CHECK: Isolation: global_actor. type: MainActor
// CHECK: sil hidden [ossa] @$s14attr_execution17testInheritsActor2fnyyyYaYbXE_tYaF : $@convention(thin) @async (@guaranteed @noescape @Sendable @async @callee_guaranteed () -> ()) -> ()
// CHECK: bb0([[FN:%.*]] : @guaranteed $@noescape @Sendable @async @callee_guaranteed () -> ()):
// CHECK: [[FN_COPY:%.*]] = copy_value [[FN]]
// CHECK: [[BORROWED_FN_COPY:%.*]] = begin_borrow [[FN_COPY]]
// CHECK: apply [[BORROWED_FN_COPY]]() : $@noescape @Sendable @async @callee_guaranteed () -> ()
// CHECK: } // end sil function '$s14attr_execution17testInheritsActor2fnyyyYaYbXE_tYaF'
@MainActor
func testInheritsActor(@_inheritActorContext(always) fn: @Sendable () async -> Void) async {
await fn()
}
6 changes: 3 additions & 3 deletions test/Concurrency/attr_execution/conversions_silgen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ func conversionsFromSyncToAsync(_ x: @escaping @Sendable (NonSendableKlass) -> V
}

func testThatClosuresAssumeIsolation(fn: inout nonisolated(nonsending) (Int) async -> Void) {
// CHECK-LABEL: sil private [ossa] @$s21attr_execution_silgen31testThatClosuresAssumeIsolation2fnyySiYaYCcz_tFyyYaYCcfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> ()
// CHECK-LABEL: sil private [ossa] @$s21attr_execution_silgen31testThatClosuresAssumeIsolation2fnyySiYaYCcz_tFyyYacfU_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> () {
// CHECK: bb0([[EXECUTOR:%.*]] : @guaranteed $Optional<any Actor>):
// CHECK: hop_to_executor [[EXECUTOR]]
let _: nonisolated(nonsending) () async -> Void = {
Expand All @@ -430,7 +430,7 @@ func testThatClosuresAssumeIsolation(fn: inout nonisolated(nonsending) (Int) asy

func testParam(_: nonisolated(nonsending) () async throws -> Void) {}

// CHECK-LABEL: sil private [ossa] @$s21attr_execution_silgen31testThatClosuresAssumeIsolation2fnyySiYaYCcz_tFyyYaYCXEfU0_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> @error any Error
// CHECK-LABEL: sil private [ossa] @$s21attr_execution_silgen31testThatClosuresAssumeIsolation2fnyySiYaYCcz_tFyyYaXEfU0_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>) -> () {
// CHECK: bb0([[EXECUTOR:%.*]] : @guaranteed $Optional<any Actor>):
// CHECK: hop_to_executor [[EXECUTOR]]
testParam { 42 }
Expand All @@ -440,7 +440,7 @@ func testThatClosuresAssumeIsolation(fn: inout nonisolated(nonsending) (Int) asy
// CHECK: hop_to_executor [[GENERIC_EXECUTOR]]
testParam { @concurrent in 42 }

// CHECK-LABEL: sil private [ossa] @$s21attr_execution_silgen31testThatClosuresAssumeIsolation2fnyySiYaYCcz_tFySiYaYCcfU2_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>, Int) -> ()
// CHECK-LABEL: sil private [ossa] @$s21attr_execution_silgen31testThatClosuresAssumeIsolation2fnyySiYaYCcz_tFySiYacfU2_ : $@convention(thin) @async (@sil_isolated @sil_implicit_leading_param @guaranteed Optional<any Actor>, Int) -> () {
// CHECK: bb0([[EXECUTOR:%.*]] : @guaranteed $Optional<any Actor>, %1 : $Int):
// CHECK: hop_to_executor [[EXECUTOR]]
fn = { _ in }
Expand Down
6 changes: 6 additions & 0 deletions test/Concurrency/attr_execution/migration_mode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,9 @@ do {
}
}
}

// @_inheritActorContext prevents `nonisolated(nonsending)` inference.
do {
func testInherit1(@_inheritActorContext _: @Sendable () async -> Void) {}
func testInherit2(@_inheritActorContext(always) _: (@Sendable () async -> Void)?) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ func testCasts() {
// expected-error@-1 {{cannot convert value of type '(nonisolated(nonsending) () async -> ()).Type' to type '(() async -> ()).Type' in coercion}}
_ = defaultedType as (nonisolated(nonsending) () async -> ()).Type // Ok
}

func test(@_inheritActorContext fn: @Sendable () async -> Void) {
let _: Int = fn
// expected-error@-1 {{cannot convert value of type '@Sendable () async -> Void' to specified type 'Int'}}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// RUN: %target-swift-frontend -emit-sil -parse-as-library -target %target-swift-5.1-abi-triple -swift-version 5 -strict-concurrency=complete %s -o - | %FileCheck %s
// RUN: %target-swift-frontend -emit-sil -parse-as-library -target %target-swift-5.1-abi-triple -swift-version 5 -strict-concurrency=complete %s -o - 2>/dev/null | %FileCheck %s
// RUN: %target-swift-frontend -emit-sil -parse-as-library -target %target-swift-5.1-abi-triple -swift-version 6 -verify %s -o /dev/null -verify-additional-prefix ni-
// RUN: %target-swift-frontend -emit-sil -parse-as-library -target %target-swift-5.1-abi-triple -swift-version 5 -strict-concurrency=complete %s -o - -enable-upcoming-feature NonisolatedNonsendingByDefault | %FileCheck %s
// RUN: %target-swift-frontend -emit-sil -parse-as-library -target %target-swift-5.1-abi-triple -swift-version 5 -strict-concurrency=complete %s -o - -enable-upcoming-feature NonisolatedNonsendingByDefault 2>/dev/null | %FileCheck %s
// RUN: %target-swift-frontend -emit-sil -parse-as-library -target %target-swift-5.1-abi-triple -swift-version 6 -verify %s -o /dev/null -enable-upcoming-feature NonisolatedNonsendingByDefault -verify-additional-prefix ni-ns-

// REQUIRES: concurrency
// REQUIRES: asserts
// REQUIRES: swift_feature_NonisolatedNonsendingByDefault

// REQUIRES: rdar154969621

// This test validates the behavior of transfernonsendable around
// closure literals

Expand Down
7 changes: 7 additions & 0 deletions test/Parse/execution_behavior_attrs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@ do {
nonisolated(0) // expected-warning {{result of call to 'nonisolated' is unused}}
print("hello")
}

do {
func testActorInheriting1(@_inheritActorContext _: @concurrent @Sendable () async -> Void) {}
// expected-error@-1 {{'@_inheritActorContext' attribute cannot be used together with '@concurrent'}}
func testActorInheriting2(@_inheritActorContext _: nonisolated(nonsending) @Sendable () async -> Void) {}
// expected-error@-1 {{'@_inheritActorContext' attribute cannot be used together with 'nonisolated(nonsending)'}}
}