Skip to content

Commit f88d8a4

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. (cherry picked from commit d2d317a)
1 parent f642519 commit f88d8a4

File tree

7 files changed

+122
-30
lines changed

7 files changed

+122
-30
lines changed

include/swift/AST/DiagnosticEngine.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,30 @@ namespace swift {
656656
InFlightDiagnostic &limitBehaviorUntilSwiftVersion(
657657
DiagnosticBehavior limit, unsigned majorVersion);
658658

659+
/// Limits the diagnostic behavior to \c limit accordingly if
660+
/// preconcurrency applies. Otherwise, the behavior limit only applies
661+
/// prior to the given language mode.
662+
///
663+
/// `@preconcurrency` applied to a nominal declaration or an import
664+
/// statement will limit concurrency diagnostic behavior based on the
665+
/// strict concurrency checking level. Under minimal checking,
666+
/// preconcurrency will suppress the diagnostics. With strict concurrency
667+
/// checking, including when building with the Swift 6 language mode,
668+
/// preconcurrency errors are downgraded to warnings. This allows libraries
669+
/// to stage in concurrency annotations even after their clients have
670+
/// migrated to Swift 6 using `@preconcurrency` alongside the newly added
671+
/// `@Sendable` or `@MainActor` annotations.
672+
InFlightDiagnostic
673+
&limitBehaviorWithPreconcurrency(DiagnosticBehavior limit,
674+
bool preconcurrency,
675+
unsigned languageMode = 6) {
676+
if (preconcurrency) {
677+
return limitBehavior(limit);
678+
}
679+
680+
return limitBehaviorUntilSwiftVersion(limit, languageMode);
681+
}
682+
659683
/// Limit the diagnostic behavior to warning until the specified version.
660684
///
661685
/// 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
@@ -788,7 +788,10 @@ static bool shouldDiagnoseExistingDataRaces(const DeclContext *dc) {
788788
});
789789
}
790790

791-
bool SendableCheckContext::isExplicitSendableConformance() const {
791+
bool SendableCheckContext::warnInMinimalChecking() const {
792+
if (preconcurrencyContext)
793+
return false;
794+
792795
if (!conformanceCheck)
793796
return false;
794797

@@ -806,7 +809,7 @@ bool SendableCheckContext::isExplicitSendableConformance() const {
806809
DiagnosticBehavior SendableCheckContext::defaultDiagnosticBehavior() const {
807810
// If we're not supposed to diagnose existing data races from this context,
808811
// ignore the diagnostic entirely.
809-
if (!isExplicitSendableConformance() &&
812+
if (!warnInMinimalChecking() &&
810813
!shouldDiagnoseExistingDataRaces(fromDC))
811814
return DiagnosticBehavior::Ignore;
812815

@@ -825,9 +828,7 @@ SendableCheckContext::implicitSendableDiagnosticBehavior() const {
825828
LLVM_FALLTHROUGH;
826829

827830
case StrictConcurrency::Minimal:
828-
// Explicit Sendable conformances always diagnose, even when strict
829-
// strict checking is disabled.
830-
if (isExplicitSendableConformance())
831+
if (warnInMinimalChecking())
831832
return DiagnosticBehavior::Warning;
832833

833834
return DiagnosticBehavior::Ignore;
@@ -868,6 +869,13 @@ bool swift::hasExplicitSendableConformance(NominalTypeDecl *nominal,
868869
/// nominal type.
869870
DiagnosticBehavior SendableCheckContext::diagnosticBehavior(
870871
NominalTypeDecl *nominal) const {
872+
// If we're in a preconcurrency context, don't override the default behavior
873+
// based on explicit conformances. For example, a @preconcurrency @Sendable
874+
// closure should not warn about an explicitly unavailable Sendable
875+
// conformance in minimal checking.
876+
if (preconcurrencyContext)
877+
return defaultDiagnosticBehavior();
878+
871879
if (hasExplicitSendableConformance(nominal))
872880
return DiagnosticBehavior::Warning;
873881

@@ -2780,11 +2788,16 @@ namespace {
27802788
continue;
27812789

27822790
auto *closure = localFunc.getAbstractClosureExpr();
2791+
auto *explicitClosure = dyn_cast_or_null<ClosureExpr>(closure);
2792+
2793+
bool preconcurrency = false;
2794+
if (explicitClosure) {
2795+
preconcurrency = explicitClosure->isIsolatedByPreconcurrency();
2796+
}
27832797

27842798
// Diagnose a `self` capture inside an escaping `sending`
27852799
// `@Sendable` closure in a deinit, which almost certainly
27862800
// means `self` would escape deinit at runtime.
2787-
auto *explicitClosure = dyn_cast_or_null<ClosureExpr>(closure);
27882801
auto *dc = getDeclContext();
27892802
if (explicitClosure && isa<DestructorDecl>(dc) &&
27902803
!explicitClosure->getType()->isNoEscape() &&
@@ -2794,7 +2807,8 @@ namespace {
27942807
if (var && var->isSelfParameter()) {
27952808
ctx.Diags.diagnose(explicitClosure->getLoc(),
27962809
diag::self_capture_deinit_task)
2797-
.warnUntilSwiftVersion(6);
2810+
.limitBehaviorWithPreconcurrency(DiagnosticBehavior::Warning,
2811+
preconcurrency);
27982812
}
27992813
}
28002814

@@ -2822,6 +2836,9 @@ namespace {
28222836
if (type->hasError())
28232837
continue;
28242838

2839+
SendableCheckContext sendableContext(getDeclContext(),
2840+
preconcurrency);
2841+
28252842
if (closure && closure->isImplicit()) {
28262843
auto *patternBindingDecl = getTopPatternBindingDecl();
28272844
if (patternBindingDecl && patternBindingDecl->isAsyncLet()) {
@@ -2843,21 +2860,21 @@ namespace {
28432860
} else {
28442861
// Fallback to a generic implicit capture missing sendable
28452862
// conformance diagnostic.
2846-
diagnoseNonSendableTypes(type, getDeclContext(),
2863+
diagnoseNonSendableTypes(type, sendableContext,
28472864
/*inDerivedConformance*/Type(),
28482865
capture.getLoc(),
28492866
diag::implicit_non_sendable_capture,
28502867
decl->getName());
28512868
}
28522869
} else if (fnType->isSendable()) {
2853-
diagnoseNonSendableTypes(type, getDeclContext(),
2870+
diagnoseNonSendableTypes(type, sendableContext,
28542871
/*inDerivedConformance*/Type(),
28552872
capture.getLoc(),
28562873
diag::non_sendable_capture,
28572874
decl->getName(),
28582875
/*closure=*/closure != nullptr);
28592876
} else {
2860-
diagnoseNonSendableTypes(type, getDeclContext(),
2877+
diagnoseNonSendableTypes(type, sendableContext,
28612878
/*inDerivedConformance*/Type(),
28622879
capture.getLoc(),
28632880
diag::non_sendable_isolated_capture,
@@ -4090,7 +4107,12 @@ namespace {
40904107
if (!mayExecuteConcurrentlyWith(dc, findCapturedDeclContext(value)))
40914108
return false;
40924109

4093-
SendableCheckContext sendableBehavior(dc);
4110+
bool preconcurrency = false;
4111+
if (auto *closure = dyn_cast<ClosureExpr>(dc)) {
4112+
preconcurrency = closure->isIsolatedByPreconcurrency();
4113+
}
4114+
4115+
SendableCheckContext sendableBehavior(dc, preconcurrency);
40944116
auto limit = sendableBehavior.defaultDiagnosticBehavior();
40954117

40964118
// Check whether this is a local variable, in which case we can
@@ -4120,7 +4142,7 @@ namespace {
41204142
ctx.Diags
41214143
.diagnose(loc, diag::concurrent_access_of_inout_param,
41224144
param->getName())
4123-
.limitBehaviorUntilSwiftVersion(limit, 6);
4145+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41244146
return true;
41254147
}
41264148
}
@@ -4135,7 +4157,7 @@ namespace {
41354157
loc, diag::concurrent_access_of_local_capture,
41364158
parent.dyn_cast<LoadExpr *>(),
41374159
var)
4138-
.limitBehaviorUntilSwiftVersion(limit, 6);
4160+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41394161
return true;
41404162
}
41414163

@@ -4145,7 +4167,7 @@ namespace {
41454167

41464168
func->diagnose(diag::local_function_executed_concurrently, func)
41474169
.fixItInsert(func->getAttributeInsertionLoc(false), "@Sendable ")
4148-
.limitBehaviorUntilSwiftVersion(limit, 6);
4170+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41494171

41504172
// Add the @Sendable attribute implicitly, so we don't diagnose
41514173
// again.
@@ -4155,7 +4177,8 @@ namespace {
41554177
}
41564178

41574179
// Concurrent access to some other local.
4158-
ctx.Diags.diagnose(loc, diag::concurrent_access_local, value);
4180+
ctx.Diags.diagnose(loc, diag::concurrent_access_local, value)
4181+
.limitBehaviorWithPreconcurrency(limit, preconcurrency);
41594182
value->diagnose(
41604183
diag::kind_declared_here, value->getDescriptiveKind());
41614184
return true;
@@ -5980,7 +6003,8 @@ bool swift::contextRequiresStrictConcurrencyChecking(
59806003
const DeclContext *dc,
59816004
llvm::function_ref<Type(const AbstractClosureExpr *)> getType,
59826005
llvm::function_ref<bool(const ClosureExpr *)> isolatedByPreconcurrency) {
5983-
switch (dc->getASTContext().LangOpts.StrictConcurrencyLevel) {
6006+
auto concurrencyLevel = dc->getASTContext().LangOpts.StrictConcurrencyLevel;
6007+
switch (concurrencyLevel) {
59846008
case StrictConcurrency::Complete:
59856009
return true;
59866010

@@ -6000,7 +6024,20 @@ bool swift::contextRequiresStrictConcurrencyChecking(
60006024

60016025
// Don't take any more cues if this only got its type information by
60026026
// being provided to a `@preconcurrency` operation.
6027+
//
6028+
// FIXME: contextRequiresStrictConcurrencyChecking is called from
6029+
// within the constraint system, but closures are only set to be isolated
6030+
// by preconcurrency in solution application because it's dependent on
6031+
// overload resolution. The constraint system either needs to check its
6032+
// own state on the current path, or not make type inference decisions based
6033+
// on concurrency checking level.
60036034
if (isolatedByPreconcurrency(explicitClosure)) {
6035+
// If we're in minimal checking, preconcurrency always suppresses
6036+
// diagnostics. Targeted checking will still produce diagnostics if
6037+
// the outer context has adopted explicit concurrency features.
6038+
if (concurrencyLevel == StrictConcurrency::Minimal)
6039+
return false;
6040+
60046041
dc = dc->getParent();
60056042
continue;
60066043
}

lib/Sema/TypeCheckConcurrency.h

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,12 +374,23 @@ static inline bool isImplicitSendableCheck(SendableCheck check) {
374374
/// Describes the context in which a \c Sendable check occurs.
375375
struct SendableCheckContext {
376376
const DeclContext * const fromDC;
377+
bool preconcurrencyContext;
377378
const std::optional<SendableCheck> conformanceCheck;
378379

379380
SendableCheckContext(
380381
const DeclContext *fromDC,
381382
std::optional<SendableCheck> conformanceCheck = std::nullopt)
382-
: fromDC(fromDC), conformanceCheck(conformanceCheck) {}
383+
: fromDC(fromDC),
384+
preconcurrencyContext(false),
385+
conformanceCheck(conformanceCheck) {}
386+
387+
SendableCheckContext(
388+
const DeclContext *fromDC,
389+
bool preconcurrencyContext,
390+
std::optional<SendableCheck> conformanceCheck = std::nullopt)
391+
: fromDC(fromDC),
392+
preconcurrencyContext(preconcurrencyContext),
393+
conformanceCheck(conformanceCheck) {}
383394

384395
/// Determine the default diagnostic behavior for a missing/unavailable
385396
/// Sendable conformance in this context.
@@ -396,8 +407,8 @@ struct SendableCheckContext {
396407
Decl *decl,
397408
bool ignoreExplicitConformance = false) const;
398409

399-
/// Whether we are in an explicit conformance to Sendable.
400-
bool isExplicitSendableConformance() const;
410+
/// Whether to warn about a Sendable violation even in minimal checking.
411+
bool warnInMinimalChecking() const;
401412
};
402413

403414
/// Diagnose any non-Sendable types that occur within the given type, using
@@ -443,11 +454,14 @@ bool diagnoseNonSendableTypes(
443454
return diagnoseNonSendableTypes(
444455
type, fromContext, derivedConformance, typeLoc,
445456
[&](Type specificType, DiagnosticBehavior behavior) {
457+
// FIXME: Reconcile preconcurrency declaration vs preconcurrency
458+
// import behavior.
446459
auto preconcurrency =
447460
fromContext.preconcurrencyBehavior(specificType->getAnyNominal());
448461

449462
ctx.Diags.diagnose(diagnoseLoc, diag, type, diagArgs...)
450-
.limitBehaviorUntilSwiftVersion(behavior, 6)
463+
.limitBehaviorWithPreconcurrency(behavior,
464+
fromContext.preconcurrencyContext)
451465
.limitBehaviorIf(preconcurrency);
452466

453467
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 & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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
76
// REQUIRES: asserts
@@ -31,7 +30,7 @@ func test() {
3130
var mutableVariable = 0
3231
preconcurrencyFunc {
3332
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}}
33+
// expected-complete-tns-warning @-1 {{mutation of captured var 'mutableVariable' in concurrently-executing code}}
3534
}
3635
mutableVariable += 1
3736
}
@@ -49,7 +48,7 @@ func testAsync() async {
4948

5049
var mutableVariable = 0
5150
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}}
51+
mutableVariable += 1 // expected-targeted-complete-warning{{mutation of captured var 'mutableVariable' in concurrently-executing code}}
5352
}
5453
mutableVariable += 1
5554
}

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)