Skip to content

Commit d2d317a

Browse files
committed
[Concurrency] Fix preconcurrency downgrade behavior for Sendable closures.
Sendable violations inside `@preconcurrency @Sendable` closures should be suppressed in minimal checking, and diagnosed as warnings under complete checking, including the Swift 6 language mode.
1 parent c0d64ba commit d2d317a

File tree

7 files changed

+122
-31
lines changed

7 files changed

+122
-31
lines changed

include/swift/AST/DiagnosticEngine.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,30 @@ namespace swift {
686686
InFlightDiagnostic &limitBehaviorUntilSwiftVersion(
687687
DiagnosticBehavior limit, unsigned majorVersion);
688688

689+
/// Limits the diagnostic behavior to \c limit accordingly if
690+
/// preconcurrency applies. Otherwise, the behavior limit only applies
691+
/// prior to the given language mode.
692+
///
693+
/// `@preconcurrency` applied to a nominal declaration or an import
694+
/// statement will limit concurrency diagnostic behavior based on the
695+
/// strict concurrency checking level. Under minimal checking,
696+
/// preconcurrency will suppress the diagnostics. With strict concurrency
697+
/// checking, including when building with the Swift 6 language mode,
698+
/// preconcurrency errors are downgraded to warnings. This allows libraries
699+
/// to stage in concurrency annotations even after their clients have
700+
/// migrated to Swift 6 using `@preconcurrency` alongside the newly added
701+
/// `@Sendable` or `@MainActor` annotations.
702+
InFlightDiagnostic
703+
&limitBehaviorWithPreconcurrency(DiagnosticBehavior limit,
704+
bool preconcurrency,
705+
unsigned languageMode = 6) {
706+
if (preconcurrency) {
707+
return limitBehavior(limit);
708+
}
709+
710+
return limitBehaviorUntilSwiftVersion(limit, languageMode);
711+
}
712+
689713
/// Limit the diagnostic behavior to warning until the specified version.
690714
///
691715
/// This helps stage in fixes for stricter diagnostics as warnings

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,10 @@ static bool shouldDiagnoseExistingDataRaces(const DeclContext *dc) {
779779
});
780780
}
781781

782-
bool SendableCheckContext::isExplicitSendableConformance() const {
782+
bool SendableCheckContext::warnInMinimalChecking() const {
783+
if (preconcurrencyContext)
784+
return false;
785+
783786
if (!conformanceCheck)
784787
return false;
785788

@@ -797,7 +800,7 @@ bool SendableCheckContext::isExplicitSendableConformance() const {
797800
DiagnosticBehavior SendableCheckContext::defaultDiagnosticBehavior() const {
798801
// If we're not supposed to diagnose existing data races from this context,
799802
// ignore the diagnostic entirely.
800-
if (!isExplicitSendableConformance() &&
803+
if (!warnInMinimalChecking() &&
801804
!shouldDiagnoseExistingDataRaces(fromDC))
802805
return DiagnosticBehavior::Ignore;
803806

@@ -816,9 +819,7 @@ SendableCheckContext::implicitSendableDiagnosticBehavior() const {
816819
LLVM_FALLTHROUGH;
817820

818821
case StrictConcurrency::Minimal:
819-
// Explicit Sendable conformances always diagnose, even when strict
820-
// strict checking is disabled.
821-
if (isExplicitSendableConformance())
822+
if (warnInMinimalChecking())
822823
return DiagnosticBehavior::Warning;
823824

824825
return DiagnosticBehavior::Ignore;
@@ -832,6 +833,13 @@ SendableCheckContext::implicitSendableDiagnosticBehavior() const {
832833
/// nominal type.
833834
DiagnosticBehavior SendableCheckContext::diagnosticBehavior(
834835
NominalTypeDecl *nominal) const {
836+
// If we're in a preconcurrency context, don't override the default behavior
837+
// based on explicit conformances. For example, a @preconcurrency @Sendable
838+
// closure should not warn about an explicitly unavailable Sendable
839+
// conformance in minimal checking.
840+
if (preconcurrencyContext)
841+
return defaultDiagnosticBehavior();
842+
835843
if (hasExplicitSendableConformance(nominal))
836844
return DiagnosticBehavior::Warning;
837845

@@ -2759,11 +2767,16 @@ namespace {
27592767
continue;
27602768

27612769
auto *closure = localFunc.getAbstractClosureExpr();
2770+
auto *explicitClosure = dyn_cast_or_null<ClosureExpr>(closure);
2771+
2772+
bool preconcurrency = false;
2773+
if (explicitClosure) {
2774+
preconcurrency = explicitClosure->isIsolatedByPreconcurrency();
2775+
}
27622776

27632777
// Diagnose a `self` capture inside an escaping `sending`
27642778
// `@Sendable` closure in a deinit, which almost certainly
27652779
// means `self` would escape deinit at runtime.
2766-
auto *explicitClosure = dyn_cast_or_null<ClosureExpr>(closure);
27672780
auto *dc = getDeclContext();
27682781
if (explicitClosure && isa<DestructorDecl>(dc) &&
27692782
!explicitClosure->getType()->isNoEscape() &&
@@ -2773,7 +2786,8 @@ namespace {
27732786
if (var && var->isSelfParameter()) {
27742787
ctx.Diags.diagnose(explicitClosure->getLoc(),
27752788
diag::self_capture_deinit_task)
2776-
.warnUntilSwiftVersion(6);
2789+
.limitBehaviorWithPreconcurrency(DiagnosticBehavior::Warning,
2790+
preconcurrency);
27772791
}
27782792
}
27792793

@@ -2801,6 +2815,9 @@ namespace {
28012815
if (type->hasError())
28022816
continue;
28032817

2818+
SendableCheckContext sendableContext(getDeclContext(),
2819+
preconcurrency);
2820+
28042821
if (closure && closure->isImplicit()) {
28052822
auto *patternBindingDecl = getTopPatternBindingDecl();
28062823
if (patternBindingDecl && patternBindingDecl->isAsyncLet()) {
@@ -2811,20 +2828,20 @@ namespace {
28112828

28122829
// Fallback to a generic implicit capture missing sendable
28132830
// conformance diagnostic.
2814-
diagnoseNonSendableTypes(type, getDeclContext(),
2831+
diagnoseNonSendableTypes(type, sendableContext,
28152832
/*inDerivedConformance*/Type(),
28162833
capture.getLoc(),
28172834
diag::implicit_non_sendable_capture,
28182835
decl->getName());
28192836
} else if (fnType->isSendable()) {
2820-
diagnoseNonSendableTypes(type, getDeclContext(),
2837+
diagnoseNonSendableTypes(type, sendableContext,
28212838
/*inDerivedConformance*/Type(),
28222839
capture.getLoc(),
28232840
diag::non_sendable_capture,
28242841
decl->getName(),
28252842
/*closure=*/closure != nullptr);
28262843
} else {
2827-
diagnoseNonSendableTypes(type, getDeclContext(),
2844+
diagnoseNonSendableTypes(type, sendableContext,
28282845
/*inDerivedConformance*/Type(),
28292846
capture.getLoc(),
28302847
diag::non_sendable_isolated_capture,
@@ -4066,7 +4083,12 @@ namespace {
40664083
if (!mayExecuteConcurrentlyWith(dc, findCapturedDeclContext(value)))
40674084
return false;
40684085

4069-
SendableCheckContext sendableBehavior(dc);
4086+
bool preconcurrency = false;
4087+
if (auto *closure = dyn_cast<ClosureExpr>(dc)) {
4088+
preconcurrency = closure->isIsolatedByPreconcurrency();
4089+
}
4090+
4091+
SendableCheckContext sendableBehavior(dc, preconcurrency);
40704092
auto limit = sendableBehavior.defaultDiagnosticBehavior();
40714093

40724094
// Check whether this is a local variable, in which case we can
@@ -4096,7 +4118,7 @@ namespace {
40964118
ctx.Diags
40974119
.diagnose(loc, diag::concurrent_access_of_inout_param,
40984120
param->getName())
4099-
.limitBehaviorUntilSwiftVersion(limit, 6);
4121+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41004122
return true;
41014123
}
41024124
}
@@ -4111,7 +4133,7 @@ namespace {
41114133
loc, diag::concurrent_access_of_local_capture,
41124134
parent.dyn_cast<LoadExpr *>(),
41134135
var)
4114-
.limitBehaviorUntilSwiftVersion(limit, 6);
4136+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41154137
return true;
41164138
}
41174139

@@ -4121,7 +4143,7 @@ namespace {
41214143

41224144
func->diagnose(diag::local_function_executed_concurrently, func)
41234145
.fixItInsert(func->getAttributeInsertionLoc(false), "@Sendable ")
4124-
.limitBehaviorUntilSwiftVersion(limit, 6);
4146+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41254147

41264148
// Add the @Sendable attribute implicitly, so we don't diagnose
41274149
// again.
@@ -4131,7 +4153,8 @@ namespace {
41314153
}
41324154

41334155
// Concurrent access to some other local.
4134-
ctx.Diags.diagnose(loc, diag::concurrent_access_local, value);
4156+
ctx.Diags.diagnose(loc, diag::concurrent_access_local, value)
4157+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41354158
value->diagnose(
41364159
diag::kind_declared_here, value->getDescriptiveKind());
41374160
return true;
@@ -6194,7 +6217,8 @@ bool swift::contextRequiresStrictConcurrencyChecking(
61946217
const DeclContext *dc,
61956218
llvm::function_ref<Type(const AbstractClosureExpr *)> getType,
61966219
llvm::function_ref<bool(const ClosureExpr *)> isolatedByPreconcurrency) {
6197-
switch (dc->getASTContext().LangOpts.StrictConcurrencyLevel) {
6220+
auto concurrencyLevel = dc->getASTContext().LangOpts.StrictConcurrencyLevel;
6221+
switch (concurrencyLevel) {
61986222
case StrictConcurrency::Complete:
61996223
return true;
62006224

@@ -6214,7 +6238,20 @@ bool swift::contextRequiresStrictConcurrencyChecking(
62146238

62156239
// Don't take any more cues if this only got its type information by
62166240
// being provided to a `@preconcurrency` operation.
6241+
//
6242+
// FIXME: contextRequiresStrictConcurrencyChecking is called from
6243+
// within the constraint system, but closures are only set to be isolated
6244+
// by preconcurrency in solution application because it's dependent on
6245+
// overload resolution. The constraint system either needs to check its
6246+
// own state on the current path, or not make type inference decisions based
6247+
// on concurrency checking level.
62176248
if (isolatedByPreconcurrency(explicitClosure)) {
6249+
// If we're in minimal checking, preconcurrency always suppresses
6250+
// diagnostics. Targeted checking will still produce diagnostics if
6251+
// the outer context has adopted explicit concurrency features.
6252+
if (concurrencyLevel == StrictConcurrency::Minimal)
6253+
return false;
6254+
62186255
dc = dc->getParent();
62196256
continue;
62206257
}

lib/Sema/TypeCheckConcurrency.h

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,12 +378,23 @@ static inline bool isImplicitSendableCheck(SendableCheck check) {
378378
/// Describes the context in which a \c Sendable check occurs.
379379
struct SendableCheckContext {
380380
const DeclContext * const fromDC;
381+
bool preconcurrencyContext;
381382
const std::optional<SendableCheck> conformanceCheck;
382383

383384
SendableCheckContext(
384385
const DeclContext *fromDC,
385386
std::optional<SendableCheck> conformanceCheck = std::nullopt)
386-
: fromDC(fromDC), conformanceCheck(conformanceCheck) {}
387+
: fromDC(fromDC),
388+
preconcurrencyContext(false),
389+
conformanceCheck(conformanceCheck) {}
390+
391+
SendableCheckContext(
392+
const DeclContext *fromDC,
393+
bool preconcurrencyContext,
394+
std::optional<SendableCheck> conformanceCheck = std::nullopt)
395+
: fromDC(fromDC),
396+
preconcurrencyContext(preconcurrencyContext),
397+
conformanceCheck(conformanceCheck) {}
387398

388399
/// Determine the default diagnostic behavior for a missing/unavailable
389400
/// Sendable conformance in this context.
@@ -400,8 +411,8 @@ struct SendableCheckContext {
400411
Decl *decl,
401412
bool ignoreExplicitConformance = false) const;
402413

403-
/// Whether we are in an explicit conformance to Sendable.
404-
bool isExplicitSendableConformance() const;
414+
/// Whether to warn about a Sendable violation even in minimal checking.
415+
bool warnInMinimalChecking() const;
405416
};
406417

407418
/// Diagnose any non-Sendable types that occur within the given type, using
@@ -447,11 +458,14 @@ bool diagnoseNonSendableTypes(
447458
return diagnoseNonSendableTypes(
448459
type, fromContext, derivedConformance, typeLoc,
449460
[&](Type specificType, DiagnosticBehavior behavior) {
461+
// FIXME: Reconcile preconcurrency declaration vs preconcurrency
462+
// import behavior.
450463
auto preconcurrency =
451464
fromContext.preconcurrencyBehavior(specificType->getAnyNominal());
452465

453466
ctx.Diags.diagnose(diagnoseLoc, diag, type, diagArgs...)
454-
.limitBehaviorUntilSwiftVersion(behavior, 6)
467+
.limitBehaviorWithPreconcurrency(behavior,
468+
fromContext.preconcurrencyContext)
455469
.limitBehaviorIf(preconcurrency);
456470

457471
return (behavior == DiagnosticBehavior::Ignore ||

test/Concurrency/Runtime/async_stream.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ class NotSendable {}
2121
@MainActor func testWarnings() {
2222
var x = 0
2323
_ = AsyncStream {
24-
x += 1 // expected-warning {{mutation of captured var 'x' in concurrently-executing code; this is an error in the Swift 6 language mode}}
24+
x += 1 // expected-warning {{mutation of captured var 'x' in concurrently-executing code}}
2525
return 0
2626
}
2727

2828
_ = AsyncThrowingStream {
29-
x += 1 // expected-warning {{mutation of captured var 'x' in concurrently-executing code; this is an error in the Swift 6 language mode}}
29+
x += 1 // expected-warning {{mutation of captured var 'x' in concurrently-executing code}}
3030
return
3131
}
3232
}

test/Concurrency/preconcurrency_typealias.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
// RUN: %target-swift-frontend -emit-sil -o /dev/null -verify %s
2-
// RUN: %target-swift-frontend -emit-sil -o /dev/null -verify %s -strict-concurrency=targeted
3-
// RUN: %target-swift-frontend -emit-sil -o /dev/null -verify %s -verify-additional-prefix complete-tns- -strict-concurrency=complete
4-
// RUN: %target-swift-frontend -emit-sil -o /dev/null -verify %s -verify-additional-prefix complete-tns- -strict-concurrency=complete -enable-upcoming-feature RegionBasedIsolation
1+
// RUN: %target-swift-frontend -emit-sil -o /dev/null -verify %s -strict-concurrency=minimal
2+
// RUN: %target-swift-frontend -emit-sil -o /dev/null -verify %s -strict-concurrency=targeted -verify-additional-prefix targeted-complete-
3+
// RUN: %target-swift-frontend -emit-sil -o /dev/null -verify %s -strict-concurrency=complete -verify-additional-prefix targeted-complete- -verify-additional-prefix complete-tns-
54

65
// REQUIRES: concurrency
7-
// REQUIRES: swift_feature_RegionBasedIsolation
86

97
@preconcurrency @MainActor func f() { }
108
// expected-note @-1 2{{calls to global function 'f()' from outside of its actor context are implicitly asynchronous}}
@@ -31,7 +29,7 @@ func test() {
3129
var mutableVariable = 0
3230
preconcurrencyFunc {
3331
mutableVariable += 1 // no sendable warning unless we have complete
34-
// expected-complete-tns-warning @-1 {{mutation of captured var 'mutableVariable' in concurrently-executing code; this is an error in the Swift 6 language mode}}
32+
// expected-complete-tns-warning @-1 {{mutation of captured var 'mutableVariable' in concurrently-executing code}}
3533
}
3634
mutableVariable += 1
3735
}
@@ -49,7 +47,7 @@ func testAsync() async {
4947

5048
var mutableVariable = 0
5149
preconcurrencyFunc {
52-
mutableVariable += 1 // expected-warning{{mutation of captured var 'mutableVariable' in concurrently-executing code; this is an error in the Swift 6 language mode}}
50+
mutableVariable += 1 // expected-targeted-complete-warning{{mutation of captured var 'mutableVariable' in concurrently-executing code}}
5351
}
5452
mutableVariable += 1
5553
}

test/Concurrency/sendable_checking_captures_swift5.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,21 @@ do {
5151
}
5252
}
5353
}
54+
55+
class NotSendable {} // expected-complete-note {{class 'NotSendable' does not conform to the 'Sendable' protocol}}
56+
57+
@available(*, unavailable)
58+
extension NotSendable : @unchecked Sendable {}
59+
60+
@preconcurrency func withSendableClosure(_: @Sendable () -> Void) {}
61+
62+
func testPreconcurrencyDowngrade(ns: NotSendable) {
63+
var x = 0
64+
withSendableClosure {
65+
_ = ns
66+
// expected-complete-warning@-1 {{capture of 'ns' with non-sendable type 'NotSendable' in a `@Sendable` closure}}
67+
68+
x += 1
69+
// expected-complete-warning@-1 {{mutation of captured var 'x' in concurrently-executing code}}
70+
}
71+
}

test/Concurrency/sendable_checking_captures_swift6.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ do {
4545

4646
sendable_preconcurrency {
4747
test.update()
48-
// expected-error@-1 {{capture of 'test' with non-sendable type 'Test' in a `@Sendable` closure}}
49-
// expected-error@-2 {{mutable capture of 'inout' parameter 'test' is not allowed in concurrently-executing code}}
48+
// expected-warning@-1 {{capture of 'test' with non-sendable type 'Test' in a `@Sendable` closure}}
49+
// expected-warning@-2 {{mutable capture of 'inout' parameter 'test' is not allowed in concurrently-executing code}}
5050
}
5151
}
5252
}

0 commit comments

Comments
 (0)