Skip to content

Commit aae9f43

Browse files
author
jturcotti
committed
Write PartitionUtils.h, implementing common utilities for manipulating a partition data structure that will be used for flow-sensitive, region-based Sendable checking.
1 parent 8f64bce commit aae9f43

File tree

5 files changed

+486
-1
lines changed

5 files changed

+486
-1
lines changed

include/swift/AST/Expr.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4649,7 +4649,7 @@ struct ApplyIsolationCrossing {
46494649

46504650
// Whether to use the isolation of the caller or callee for generating
46514651
// informative diagnostics depends on whether this crossing is an exit.
4652-
// In particular, we tend to use the caller isolation for diagnostics,
4652+
// In particular, we tend to use the callee isolation for diagnostics,
46534653
// but if this crossing is an exit from isolation then the callee isolation
46544654
// is not very informative, so we use the caller isolation instead.
46554655
ActorIsolation getDiagnoseIsolation() const {
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#ifndef SWIFT_PARTITIONUTILS_H
2+
#define SWIFT_PARTITIONUTILS_H
3+
4+
#include "llvm/Support/Debug.h"
5+
#include "llvm/ADT/SmallVector.h"
6+
7+
namespace swift {
8+
9+
enum class PartitionOpKind : uint8_t {
10+
Assign,
11+
AssignFresh,
12+
Consume,
13+
Merge,
14+
Require
15+
};
16+
17+
class PartitionOp {
18+
private:
19+
PartitionOpKind OpKind;
20+
llvm::SmallVector<unsigned, 2> OpArgs;
21+
22+
// TODO: can the following two declarations be merged?
23+
PartitionOp(PartitionOpKind OpKind, unsigned arg1)
24+
: OpKind(OpKind), OpArgs({arg1}) {}
25+
26+
PartitionOp(PartitionOpKind OpKind, unsigned arg1, unsigned arg2)
27+
: OpKind(OpKind), OpArgs({arg1, arg2}) {}
28+
29+
friend class Partition;
30+
31+
public:
32+
static PartitionOp Assign(unsigned tgt, unsigned src) {
33+
return PartitionOp(PartitionOpKind::Assign, tgt, src);
34+
}
35+
36+
static PartitionOp AssignFresh(unsigned tgt) {
37+
return PartitionOp(PartitionOpKind::AssignFresh, tgt);
38+
}
39+
40+
static PartitionOp Consume(unsigned tgt) {
41+
return PartitionOp(PartitionOpKind::Consume, tgt);
42+
}
43+
44+
static PartitionOp Merge(unsigned tgt1, unsigned tgt2) {
45+
return PartitionOp(PartitionOpKind::Merge, tgt1, tgt2);
46+
}
47+
48+
static PartitionOp Require(unsigned tgt) {
49+
return PartitionOp(PartitionOpKind::Require, tgt);
50+
}
51+
};
52+
53+
// For the passed `map`, ensure that `key` maps to `val`. If `key` already
54+
// mapped to a different value, ensure that all other keys mapped to that
55+
// value also now map to `val`. This is a relatively expensive (linear time)
56+
// operation that's unfortunately used pervasively throughout PartitionOp
57+
// application. If this is a performance bottleneck, let's consider optimizing
58+
// it to a true union-find or other tree-based data structure.
59+
static void horizontalUpdate(std::map<unsigned, signed> &map, unsigned key,
60+
signed val) {
61+
if (!map.count(key)) {
62+
map[key] = val;
63+
return;
64+
}
65+
66+
signed oldVal = map[key];
67+
68+
for (auto [otherKey, otherVal] : map)
69+
if (otherVal == oldVal)
70+
map[otherKey] = val;
71+
}
72+
73+
class Partition {
74+
private:
75+
std::map<unsigned, signed> labels = {};
76+
77+
bool canonical = true;
78+
79+
// linear time - For each region label that occurs, find the first index
80+
// at which it occurs and relabel all instances of it to that index.
81+
// This excludes the -1 label for missing region.
82+
void canonicalize() {
83+
if (canonical)
84+
return;
85+
canonical = true;
86+
87+
std::map<signed, unsigned> relabel;
88+
89+
for (auto &[i, label] : labels) {
90+
// leave -1 (missing region) as is
91+
if (label < 0)
92+
continue;
93+
94+
if (!relabel.count(label)) {
95+
// if this is the first time encountering this region label,
96+
// then this region label should be relabelled to this index,
97+
// so enter that into the map
98+
relabel[label] = i;
99+
}
100+
101+
label = relabel[label];
102+
}
103+
}
104+
105+
public:
106+
void dump() const {
107+
llvm::dbgs() << "Partition";
108+
if (canonical)
109+
llvm::dbgs() << "(canonical)";
110+
llvm::dbgs() << "{";
111+
for (const auto &[i, label] : labels)
112+
llvm::dbgs() << "[" << i << ": " << label << "] ";
113+
llvm::dbgs() << "}\n";
114+
}
115+
116+
// linear time - Test two partititons for equality by first putting them
117+
// in canonical form then comparing for exact equality.
118+
static bool equals(Partition &fst, Partition &snd) {
119+
fst.canonicalize();
120+
snd.canonicalize();
121+
122+
return fst.labels == snd.labels;
123+
}
124+
125+
// quadratic time - Construct the partition corresponding to the join of the
126+
// two passed partitions; the join labels each index labelled by both operands
127+
// and two indices are in the same region of the join iff they are in the same
128+
// region in either operand.
129+
static Partition join(Partition &fst, Partition &snd) {
130+
fst.canonicalize();
131+
snd.canonicalize();
132+
133+
std::map<unsigned, signed> relabel_fst;
134+
std::map<unsigned, signed> relabel_snd;
135+
auto lookup_fst = [&](unsigned i) {
136+
// signed to unsigned conversion... ?
137+
return relabel_fst.count(fst.labels[i]) ? relabel_fst[fst.labels[i]]
138+
: fst.labels[i];
139+
};
140+
141+
auto lookup_snd = [&](unsigned i) {
142+
// signed to unsigned conversion... safe?
143+
return relabel_snd.count(snd.labels[i]) ? relabel_snd[snd.labels[i]]
144+
: snd.labels[i];
145+
};
146+
147+
for (const auto &[i, _] : fst.labels) {
148+
// only consider indices present in both fst and snd
149+
if (!snd.labels.count(i))
150+
continue;
151+
152+
signed label_joined = std::min(lookup_fst(i), lookup_snd(i));
153+
154+
horizontalUpdate(relabel_fst, fst.labels[i], label_joined);
155+
horizontalUpdate(relabel_snd, snd.labels[i], label_joined);
156+
}
157+
158+
Partition joined;
159+
joined.canonical = true;
160+
for (const auto &[i, _] : fst.labels) {
161+
if (!snd.labels.count(i))
162+
continue;
163+
joined.labels[i] = lookup_fst(i);
164+
}
165+
166+
return joined;
167+
}
168+
169+
// It's possible for all PartitionOps' to maintain canonicality,
170+
// but it comes at the cost of making Assign operations worst-case
171+
// linear time instead of constant. This is likely not worth it,
172+
// so it's disabled by default, but leaving this flag here in case
173+
// it becomes useful as a performance optimization.
174+
static const bool ALWAYS_CANONICAL = false;
175+
176+
void apply(
177+
PartitionOp op, std::function<void(const PartitionOp &)> handleFailure =
178+
[](const PartitionOp &_) {}) {
179+
switch (op.OpKind) {
180+
case PartitionOpKind::Assign:
181+
assert(op.OpArgs.size() == 2 &&
182+
"Assign PartitionOp should be passed 2 arguments");
183+
assert(labels.count(op.OpArgs[0]) && labels.count(op.OpArgs[1]) &&
184+
"Assign PartitionOp's arguments should be already tracked");
185+
// if assigning to a missing region, handle the failure
186+
if (labels[op.OpArgs[1]] < 0)
187+
handleFailure(op);
188+
189+
labels[op.OpArgs[0]] = labels[op.OpArgs[1]];
190+
191+
if (ALWAYS_CANONICAL) {
192+
// if seeking to maintain canonicality, then do so
193+
if (op.OpArgs[0] < labels[op.OpArgs[0]])
194+
horizontalUpdate(labels, op.OpArgs[0], op.OpArgs[0]);
195+
break;
196+
}
197+
198+
// assignment could have invalidated canonicality
199+
canonical = false;
200+
break;
201+
case PartitionOpKind::AssignFresh:
202+
assert(op.OpArgs.size() == 1 &&
203+
"AssignFresh PartitionOp should be passed 1 argument");
204+
assert(!labels.count(op.OpArgs[0]) &&
205+
"AssignFresh PartitionOp's argument should NOT already be tracked");
206+
207+
// fresh region generated by mapping the passed index to itself
208+
labels[op.OpArgs[0]] = op.OpArgs[0];
209+
break;
210+
case PartitionOpKind::Consume:
211+
assert(op.OpArgs.size() == 1 &&
212+
"Consume PartitionOp should be passed 1 argument");
213+
assert(labels.count(op.OpArgs[0]) &&
214+
"Consume PartitionOp's argument should already be tracked");
215+
216+
// if attempting to consume a missing region, handle the failure
217+
if (labels[op.OpArgs[0]] < 0)
218+
handleFailure(op);
219+
220+
// mark region as missing
221+
horizontalUpdate(labels, op.OpArgs[0], -1);
222+
break;
223+
case PartitionOpKind::Merge:
224+
assert(op.OpArgs.size() == 2 &&
225+
"Merge PartitionOp should be passed 2 arguments");
226+
assert(labels.count(op.OpArgs[0]) && labels.count(op.OpArgs[1]) &&
227+
"Merge PartitionOp's arguments should already be tracked");
228+
// if attempting to merge a missing region, handle the failure
229+
if (labels[op.OpArgs[0]] < 0 || labels[op.OpArgs[1]] < 0)
230+
handleFailure(op);
231+
232+
if (labels[op.OpArgs[0]] == labels[op.OpArgs[1]])
233+
break;
234+
235+
// maintain canonicality by renaming the greater-numbered region
236+
if (labels[op.OpArgs[0]] < labels[op.OpArgs[1]])
237+
horizontalUpdate(labels, op.OpArgs[1], labels[op.OpArgs[0]]);
238+
else
239+
horizontalUpdate(labels, op.OpArgs[0], labels[op.OpArgs[1]]);
240+
break;
241+
case PartitionOpKind::Require:
242+
assert(op.OpArgs.size() == 1 &&
243+
"Require PartitionOp should be passed 1 argument");
244+
assert(labels.count(op.OpArgs[0]) &&
245+
"Require PartitionOp's argument should already be tracked");
246+
if (labels[op.OpArgs[0]] < 0)
247+
handleFailure(op);
248+
}
249+
}
250+
};
251+
}
252+
253+
#endif

lib/SILOptimizer/Mandatory/SendNonSendable.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "../../Sema/TypeCheckConcurrency.h"
22
#include "swift/AST/Expr.h"
33
#include "swift/SILOptimizer/PassManager/Transforms.h"
4+
#include "swift/SILOptimizer/Utils/PartitionUtils.h"
45

56
using namespace swift;
67

unittests/SIL/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
add_swift_unittest(SwiftSILTests
2+
PartitionUtilsTest.cpp
23
SILBitfieldTest.cpp
34
)
45

0 commit comments

Comments
 (0)