Skip to content

Commit 41308d2

Browse files
committed
[region-isolation] Assigns to var with structs/tuples with multiple fields should be merges for now.
The reason that this is being done is that since currently region based isolation is not field sensitive, when we assign to the struct or tuple field of the var, the new region relationship is set for the entire struct, not just a specific field. This means that we effectively lose any region information from the other fields. For example in the following at (1), given the current rules, we lose that s.y was assigned to x: ```swift struct S { var x: NonSendableKlass var y: NonSendableKlass } func foo() { var s = S() // Regions: [s] let x = NonSendableKlass() let y = NonSendableKlass() // Regions: [(s), (x), (y)] s.y = x // Regions: [(s, x), (y)] s.x = y (1) // Regions: [(x), (s, y)] } ``` The best solution to this is to track such var like bindings in a field sensitive manner where the regions of the aggregate are the union of the individual fields. This would let us represent the regions of the above as follows: ```swift func foo() { var s = S() // Regions: [((s.x), (s.y))] let x = NonSendableKlass() let y = NonSendableKlass() // Regions: [((s.x), (s.y)), (x), (y)] s.y = x // Regions: [((s.x), (s.y, x)), (y)] s.x = y (1) // Regions: [((s.x, y), (s.y, x))] } ``` We cannot do this today so to plug this hole, we instead treat these operations as merges. This provides a conservative answer. Thus we would have:" ```swift func foo() { var s = S() // Regions: [s] let x = NonSendableKlass() let y = NonSendableKlass() // Regions: [(s), (x), (y)] s.y = x // Regions: [(s, x), (y)] s.x = y (1) // Regions: [(s, x, y]) } ``` This is because we are treating the assignment into s.y and s.x as merges into s, so we do not lose that y was assigned into s before we assigned y into it. The unfortunate side-effect of this is that if a struct or tuple has multiple fields, the merge makes it so that if we assign over the same field, we do not lose the region of the old value: ```swift func foo() { var s = S() // Regions: [s] let x = NonSendableKlass() let y = NonSendableKlass() // Regions: [(s), (x), (y)] s.y = x // Regions: [(s, x), (y)] s.y = y (1) // Regions: [(s, x, y]) } ``` If we were not to do this, then the s.y at (1) would result in s and x being in different regions. rdar://117273952
1 parent 2835bb4 commit 41308d2

File tree

2 files changed

+164
-6
lines changed

2 files changed

+164
-6
lines changed

lib/SILOptimizer/Mandatory/TransferNonSendable.cpp

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ namespace {
5555

5656
struct UseDefChainVisitor
5757
: public AccessUseDefChainVisitor<UseDefChainVisitor, SILValue> {
58+
bool isMerge = false;
5859

5960
SILValue visitAll(SILValue sourceAddr) {
6061
SILValue result = visit(sourceAddr);
@@ -91,6 +92,8 @@ struct UseDefChainVisitor
9192

9293
SILValue visitStorageCast(SingleValueInstruction *, Operand *sourceAddr,
9394
AccessStorageCast castType) {
95+
// If we do not have an identity cast, mark this as a merge.
96+
isMerge |= castType != AccessStorageCast::Identity;
9497
return sourceAddr->get();
9598
}
9699

@@ -108,13 +111,22 @@ struct UseDefChainVisitor
108111
case ProjectionKind::Class:
109112
llvm_unreachable("Shouldn't see this here");
110113
case ProjectionKind::Index:
114+
// Index is always a merge.
115+
isMerge = true;
111116
break;
112117
case ProjectionKind::Enum:
118+
// Enum is never a merge since it always has a single field.
113119
break;
114120
case ProjectionKind::Tuple: {
121+
// These are merges if we have multiple fields.
122+
auto *tti = cast<TupleElementAddrInst>(inst);
123+
isMerge |= tti->getOperand()->getType().getNumTupleElements() > 1;
115124
break;
116125
}
117126
case ProjectionKind::Struct:
127+
// These are merges if we have multiple fields.
128+
auto *sea = cast<StructElementAddrInst>(inst);
129+
isMerge |= sea->getOperand()->getType().getNumNominalFields() > 1;
118130
break;
119131
}
120132
}
@@ -163,6 +175,13 @@ static Expr *getExprForPartitionOp(const PartitionOp &op) {
163175
return expr;
164176
}
165177

178+
static bool isProjectedFromAggregate(SILValue value) {
179+
assert(value->getType().isAddress());
180+
UseDefChainVisitor visitor;
181+
visitor.visitAll(value);
182+
return visitor.isMerge;
183+
}
184+
166185
//===----------------------------------------------------------------------===//
167186
// MARK: Main Computation
168187
//===----------------------------------------------------------------------===//
@@ -873,8 +892,18 @@ class PartitionOpTranslator {
873892
/// merges, to ensure any aliases of tgt are also updated.
874893
void translateSILStore(SILValue dest, SILValue src) {
875894
if (auto nonSendableTgt = tryToTrackValue(dest)) {
876-
// Stores to unaliased storage can be treated as assignments, not merges.
877-
if (nonSendableTgt.value().isNoAlias())
895+
// In the following situations, we can perform an assign:
896+
//
897+
// 1. A store to unaliased storage.
898+
// 2. A store that is to an entire value.
899+
//
900+
// DISCUSSION: If we have case 2, we need to merge the regions since we
901+
// are not overwriting the entire region of the value. This does mean that
902+
// we artificially include the previous region that was stored
903+
// specifically in this projection... but that is better than
904+
// miscompiling. For memory like this, we probably need to track it on a
905+
// per field basis to allow for us to assign.
906+
if (nonSendableTgt.value().isNoAlias() && !isProjectedFromAggregate(dest))
878907
return translateSILAssign(dest, src);
879908

880909
// Stores to possibly aliased storage must be treated as merges.
@@ -982,8 +1011,13 @@ class PartitionOpTranslator {
9821011
// NOTE: translateSILLookThrough asserts that this property is true.
9831012
case SILInstructionKind::BeginAccessInst:
9841013
case SILInstructionKind::BeginBorrowInst:
1014+
case SILInstructionKind::BeginDeallocRefInst:
1015+
case SILInstructionKind::RefToBridgeObjectInst:
1016+
case SILInstructionKind::BridgeObjectToRefInst:
9851017
case SILInstructionKind::CopyValueInst:
1018+
case SILInstructionKind::EndCOWMutationInst:
9861019
case SILInstructionKind::ProjectBoxInst:
1020+
case SILInstructionKind::EndInitLetRefInst:
9871021
case SILInstructionKind::InitEnumDataAddrInst:
9881022
case SILInstructionKind::OpenExistentialAddrInst:
9891023
case SILInstructionKind::StructElementAddrInst:
@@ -993,6 +1027,11 @@ class PartitionOpTranslator {
9931027
case SILInstructionKind::UpcastInst:
9941028
return translateSILLookThrough(inst->getResult(0), inst->getOperand(0));
9951029

1030+
case SILInstructionKind::UnconditionalCheckedCastInst:
1031+
if (SILDynamicCastInst(inst).isRCIdentityPreserving())
1032+
return translateSILLookThrough(inst->getResult(0), inst->getOperand(0));
1033+
return translateSILAssign(inst);
1034+
9961035
// Just make the result part of the operand's region without requiring.
9971036
//
9981037
// This is appropriate for things like object casts and object
@@ -1011,7 +1050,6 @@ class PartitionOpTranslator {
10111050
case SILInstructionKind::OpenExistentialRefInst:
10121051
case SILInstructionKind::PointerToAddressInst:
10131052
case SILInstructionKind::ProjectBlockStorageInst:
1014-
case SILInstructionKind::RefElementAddrInst:
10151053
case SILInstructionKind::RefToUnmanagedInst:
10161054
case SILInstructionKind::StructExtractInst:
10171055
case SILInstructionKind::TailAddrInst:
@@ -1021,6 +1059,14 @@ class PartitionOpTranslator {
10211059
case SILInstructionKind::UncheckedEnumDataInst:
10221060
case SILInstructionKind::UncheckedOwnershipConversionInst:
10231061
case SILInstructionKind::UnmanagedToRefInst:
1062+
1063+
// RefElementAddrInst is not considered to be a lookThrough since we want to
1064+
// consider the address projected from the class to be a separate value that
1065+
// is in the same region as the parent operand. The reason that we want to
1066+
// do this is to ensure that if we assign into the ref_element_addr memory,
1067+
// we do not consider writes into the struct that contains the
1068+
// ref_element_addr to be merged into.
1069+
case SILInstructionKind::RefElementAddrInst:
10241070
return translateSILAssign(inst);
10251071

10261072
/// Enum inst is handled specially since if it does not have an argument,

test/Concurrency/sendnonsendable_basic.swift

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ func useValue<T>(_ x: T) {}
4444

4545
var booleanFlag: Bool { false }
4646

47+
struct SingleFieldKlassBox {
48+
var k = NonSendableKlass()
49+
}
50+
51+
struct TwoFieldKlassBox {
52+
var k1 = NonSendableKlass()
53+
var k2 = NonSendableKlass()
54+
}
55+
4756
////////////////////////////
4857
// MARK: Actor Self Tests //
4958
////////////////////////////
@@ -187,10 +196,9 @@ extension Actor {
187196
let closure: () -> () = {
188197
print(self.klass)
189198
}
190-
// NOTE: We do not error on this today since we assign into 1 and that makes
191-
// x assign fresh. It will be fixed in a forthcoming commit.
199+
192200
let x = (closure, 1)
193-
await transferToMain(x)
201+
await transferToMain(x) // expected-tns-warning {{call site passes `self` or a non-sendable argument of this function to another thread, potentially yielding a race with the caller}}
194202
// expected-complete-warning @-1 {{passing argument of non-sendable type '(() -> (), Int)' into main actor-isolated context may introduce data races}}
195203
// expected-complete-note @-2 {{a function type must be marked '@Sendable' to conform to 'Sendable'}}
196204
}
@@ -491,3 +499,107 @@ func testConversionsAndSendable(a: Actor, f: @Sendable () -> Void, f2: () -> Voi
491499
// expected-complete-warning @-1 {{passing argument of non-sendable type '() -> Void' into actor-isolated context may introduce data races}}
492500
// expected-complete-note @-2 {{a function type must be marked '@Sendable' to conform to 'Sendable'}}
493501
}
502+
503+
///////////////////////////////////////////////
504+
// Multiple Field Var Assignment Merge Tests //
505+
///////////////////////////////////////////////
506+
507+
func singleFieldVarMergeTest() async {
508+
var box = SingleFieldKlassBox()
509+
box = SingleFieldKlassBox()
510+
511+
// This transfers the entire region.
512+
await transferToMain(box.k)
513+
514+
// But since box has only a single element, this doesn't race.
515+
box.k = NonSendableKlass()
516+
useValue(box.k)
517+
useValue(box)
518+
519+
// We transfer the box back to main.
520+
await transferToMain(box)
521+
522+
// And reassign over the entire box, so again we can use it again.
523+
box = SingleFieldKlassBox()
524+
525+
useValue(box)
526+
useValue(box.k)
527+
528+
await transferToMain(box) // expected-tns-warning {{passing argument of non-sendable type 'SingleFieldKlassBox' from nonisolated context to main actor-isolated context at this call site could yield a race with accesses later in this function}}
529+
530+
// But if we use box.k here, we emit an error since we didn't reinitialize at
531+
// all.
532+
useValue(box.k) // expected-note {{access here could race}}
533+
}
534+
535+
func multipleFieldVarMergeTest1() async {
536+
var box = TwoFieldKlassBox()
537+
box = TwoFieldKlassBox()
538+
539+
// This transfers the entire region.
540+
await transferToMain(box.k1) // expected-tns-warning {{passing argument of non-sendable type 'NonSendableKlass' from nonisolated context to main actor-isolated context at this call site could yield a race with accesses later in this function}}
541+
542+
// So even if we reassign over k1, since we did a merge, this should error.
543+
box.k1 = NonSendableKlass() // expected-note {{access here could race}}
544+
useValue(box) // expected-note {{access here could race}}
545+
}
546+
547+
func multipleFieldVarMergeTest2() async {
548+
var box = TwoFieldKlassBox()
549+
box = TwoFieldKlassBox()
550+
551+
// This transfers the entire region.
552+
await transferToMain(box.k1)
553+
554+
// But if we assign over box completely, we can use it again.
555+
box = TwoFieldKlassBox()
556+
557+
useValue(box.k1)
558+
useValue(box.k2)
559+
useValue(box)
560+
561+
await transferToMain(box.k2)
562+
563+
// But if we assign over box completely, we can use it again.
564+
box = TwoFieldKlassBox()
565+
566+
useValue(box.k1)
567+
useValue(box.k2)
568+
useValue(box)
569+
}
570+
571+
func multipleFieldTupleMergeTest1() async {
572+
var box = (NonSendableKlass(), NonSendableKlass())
573+
box = (NonSendableKlass(), NonSendableKlass())
574+
575+
// This transfers the entire region.
576+
await transferToMain(box.0) // expected-tns-warning {{passing argument of non-sendable type 'NonSendableKlass' from nonisolated context to main actor-isolated context at this call site could yield a race with accesses later in this function}}
577+
578+
// So even if we reassign over k1, since we did a merge, this should error.
579+
box.0 = NonSendableKlass() // expected-note {{access here could race}}
580+
useValue(box) // expected-note {{access here could race}}
581+
}
582+
583+
func multipleFieldTupleMergeTest2() async {
584+
var box = (NonSendableKlass(), NonSendableKlass())
585+
586+
// This transfers the entire region.
587+
await transferToMain(box.0)
588+
589+
let box2 = (NonSendableKlass(), NonSendableKlass())
590+
// But if we assign over box completely, we can use it again.
591+
box = box2
592+
593+
useValue(box.0)
594+
useValue(box.1)
595+
useValue(box)
596+
597+
await transferToMain(box.1)
598+
599+
// But if we assign over box completely, we can use it again.
600+
box = (NonSendableKlass(), NonSendableKlass())
601+
602+
useValue(box.0)
603+
useValue(box.1)
604+
useValue(box)
605+
}

0 commit comments

Comments
 (0)