Skip to content

Commit c0b3efe

Browse files
committed
[region-isolation] Instead of using the complex logic to emit all possible requires... just emit a diagnostic on the first require that we see.
This involved me removing the complex logic for emitting diagnostics we have today in favor of a simple diagnostic that works by: 1. Instead of searching for transfer instructions, we use the transfer instruction that we propagated by the dataflow... so there is no way for us to possible not identify a transfer instruction... we always have it. 2. Instead of emitting diagnostics for all requires, just emit a warning for the first require we see along a path. The reason that we need to do this is that in certain cases we will have multiple "require" instructions from slightly different source locations (e.x.: differing by column) but on the same line. I saw this happen specifically with print where we treat stores into the array as a require as well as the actual call to print itself where we pass the array. An additional benefit of this is that this allowed me to get rid of the cache of already seen require instructions. By doing this, we now emit errors when the same apply needs to be required by different transfer instructions for different arguments. NOTE: I left in the original implementation code to make it easier to review this new code. I deleted it in the next commit. Otherwise the git diff for this patch is too difficult to read.
1 parent 95669c5 commit c0b3efe

File tree

4 files changed

+459
-166
lines changed

4 files changed

+459
-166
lines changed

lib/SILOptimizer/Mandatory/TransferNonSendable.cpp

Lines changed: 253 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,190 @@ class BlockPartitionState {
15281528

15291529
} // namespace
15301530

1531+
//===----------------------------------------------------------------------===//
1532+
// MARK: Require Liveness
1533+
//===----------------------------------------------------------------------===//
1534+
1535+
namespace {
1536+
1537+
class BlockLivenessInfo {
1538+
// Generation counter so we do not need to reallocate.
1539+
unsigned generation = 0;
1540+
SILInstruction *firstRequireInst = nullptr;
1541+
1542+
void resetIfNew(unsigned newGeneration) {
1543+
if (generation == newGeneration)
1544+
return;
1545+
generation = newGeneration;
1546+
firstRequireInst = nullptr;
1547+
}
1548+
1549+
public:
1550+
SILInstruction *getInst(unsigned callerGeneration) {
1551+
resetIfNew(callerGeneration);
1552+
return firstRequireInst;
1553+
}
1554+
1555+
void setInst(unsigned callerGeneration, SILInstruction *newValue) {
1556+
resetIfNew(callerGeneration);
1557+
firstRequireInst = newValue;
1558+
}
1559+
};
1560+
1561+
/// We only want to emit errors for the first requires along a path from a
1562+
/// transfer instruction. We discover this by walking from user blocks to
1563+
struct RequireLiveness {
1564+
unsigned generation;
1565+
SILInstruction *transferInst;
1566+
BasicBlockData<BlockLivenessInfo> &blockLivenessInfo;
1567+
InstructionSet allRequires;
1568+
InstructionSetWithSize finalRequires;
1569+
1570+
/// If we have requires in the def block before our transfer, this is the
1571+
/// first require.
1572+
SILInstruction *firstRequireBeforeTransferInDefBlock = nullptr;
1573+
1574+
RequireLiveness(unsigned generation, Operand *transferOp,
1575+
BasicBlockData<BlockLivenessInfo> &blockLivenessInfo)
1576+
: generation(generation), transferInst(transferOp->getUser()),
1577+
blockLivenessInfo(blockLivenessInfo),
1578+
allRequires(transferOp->getParentFunction()),
1579+
finalRequires(transferOp->getParentFunction()) {}
1580+
1581+
template <typename Collection>
1582+
void process(Collection collection);
1583+
1584+
/// Attempt to process requireInst for our def block. Returns false if
1585+
/// requireInst was before our def and we need to do interprocedural
1586+
/// processing. Returns true if requireInst was after our transferInst and we
1587+
/// were able to appropriately determine if we should emit it or not.
1588+
void processDefBlock();
1589+
1590+
/// Process all requires in block, updating blockLivenessInfo.
1591+
void processNonDefBlock(SILBasicBlock *block);
1592+
};
1593+
1594+
} // namespace
1595+
1596+
void RequireLiveness::processDefBlock() {
1597+
LLVM_DEBUG(llvm::dbgs() << " Processing def block!\n");
1598+
// First walk from the beginning of the block to the transfer instruction to
1599+
// see if we have any requires before our def. Once we find one, we can skip
1600+
// the traversal and jump straight to the transfer.
1601+
for (auto ii = transferInst->getParent()->begin(),
1602+
ie = transferInst->getIterator();
1603+
ii != ie; ++ii) {
1604+
if (allRequires.contains(&*ii) && !firstRequireBeforeTransferInDefBlock) {
1605+
firstRequireBeforeTransferInDefBlock = &*ii;
1606+
LLVM_DEBUG(llvm::dbgs() << " Found transfer before def: "
1607+
<< *firstRequireBeforeTransferInDefBlock);
1608+
break;
1609+
}
1610+
}
1611+
1612+
// Then walk from our transferInst to the end of the block looking for the
1613+
// first require inst. Once we find it... return.
1614+
for (auto ii = std::next(transferInst->getIterator()),
1615+
ie = transferInst->getParent()->end();
1616+
ii != ie; ++ii) {
1617+
if (!allRequires.contains(&*ii))
1618+
continue;
1619+
1620+
finalRequires.insert(&*ii);
1621+
LLVM_DEBUG(llvm::dbgs() << " Found transfer after def: " << *ii);
1622+
return;
1623+
}
1624+
}
1625+
1626+
void RequireLiveness::processNonDefBlock(SILBasicBlock *block) {
1627+
// Walk from the bottom to the top... assigning to our block state.
1628+
auto blockState = blockLivenessInfo.get(block);
1629+
for (auto &inst : llvm::make_range(block->rbegin(), block->rend())) {
1630+
if (!finalRequires.contains(&inst))
1631+
continue;
1632+
blockState.get()->setInst(generation, &inst);
1633+
}
1634+
}
1635+
1636+
template <typename Collection>
1637+
void RequireLiveness::process(Collection requireInstList) {
1638+
LLVM_DEBUG(llvm::dbgs() << "==> Performing Require Liveness for: "
1639+
<< *transferInst);
1640+
1641+
// Then put all of our requires into our allRequires set.
1642+
BasicBlockWorklist initializingWorklist(transferInst->getFunction());
1643+
for (auto *require : requireInstList) {
1644+
LLVM_DEBUG(llvm::dbgs() << " Require Inst: " << *require);
1645+
allRequires.insert(require);
1646+
initializingWorklist.pushIfNotVisited(require->getParent());
1647+
}
1648+
1649+
// Then process our def block to see if we have any requires before and after
1650+
// the transferInst...
1651+
processDefBlock();
1652+
1653+
// If we found /any/ requries after the transferInst, we can bail early since
1654+
// that is guaranteed to dominate all further requires.
1655+
if (!finalRequires.empty()) {
1656+
LLVM_DEBUG(
1657+
llvm::dbgs()
1658+
<< " Found transfer after def in def block! Exiting early!\n");
1659+
return;
1660+
}
1661+
1662+
LLVM_DEBUG(llvm::dbgs() << " Did not find transfer after def in def "
1663+
"block! Walking blocks!\n");
1664+
1665+
// If we found a transfer in the def block before our def, add it to the block
1666+
// state for the def.
1667+
if (firstRequireBeforeTransferInDefBlock) {
1668+
LLVM_DEBUG(
1669+
llvm::dbgs()
1670+
<< " Found a require before transfer! Adding to block state!\n");
1671+
auto blockState = blockLivenessInfo.get(transferInst->getParent());
1672+
blockState.get()->setInst(generation, firstRequireBeforeTransferInDefBlock);
1673+
}
1674+
1675+
// Then for each require block that isn't a def block transfer, find the
1676+
// earliest transfer inst.
1677+
while (auto *requireBlock = initializingWorklist.pop()) {
1678+
auto blockState = blockLivenessInfo.get(requireBlock);
1679+
for (auto &inst : *requireBlock) {
1680+
if (!allRequires.contains(&inst))
1681+
continue;
1682+
LLVM_DEBUG(llvm::dbgs() << " Mapping Block bb"
1683+
<< requireBlock->getDebugID() << " to: " << inst);
1684+
blockState.get()->setInst(generation, &inst);
1685+
break;
1686+
}
1687+
}
1688+
1689+
// Then walk from our def block looking for setInst blocks.
1690+
auto *transferBlock = transferInst->getParent();
1691+
BasicBlockWorklist worklist(transferInst->getFunction());
1692+
for (auto *succBlock : transferBlock->getSuccessorBlocks())
1693+
worklist.pushIfNotVisited(succBlock);
1694+
1695+
while (auto *next = worklist.pop()) {
1696+
// Check if we found an earliest requires... if so, add that to final
1697+
// requires and continue. We don't want to visit successors.
1698+
auto blockState = blockLivenessInfo.get(next);
1699+
if (auto *inst = blockState.get()->getInst(generation)) {
1700+
finalRequires.insert(inst);
1701+
continue;
1702+
}
1703+
1704+
// Do not look at successors of the transfer block.
1705+
if (next == transferBlock)
1706+
continue;
1707+
1708+
// Otherwise, we did not find a requires and need to search further
1709+
// successors.
1710+
for (auto *succBlock : next->getSuccessorBlocks())
1711+
worklist.pushIfNotVisited(succBlock);
1712+
}
1713+
}
1714+
15311715
//===----------------------------------------------------------------------===//
15321716
// MARK: Inferring Transferred Instruction from violating Require
15331717
//===----------------------------------------------------------------------===//
@@ -2035,15 +2219,10 @@ class PartitionAnalysis {
20352219

20362220
BasicBlockData<BlockPartitionState> blockStates;
20372221

2038-
RaceTracer raceTracer;
2039-
20402222
SILFunction *function;
20412223

20422224
bool solved;
20432225

2044-
// TODO: make this configurable in a better way
2045-
const static int NUM_REQUIREMENTS_TO_DIAGNOSE = 50;
2046-
20472226
/// The constructor initializes each block in the function by compiling it to
20482227
/// PartitionOps, then seeds the solve method by setting `needsUpdate` to true
20492228
/// for the entry block
@@ -2053,7 +2232,7 @@ class PartitionAnalysis {
20532232
[this](SILBasicBlock *block) {
20542233
return BlockPartitionState(block, translator);
20552234
}),
2056-
raceTracer(fn, blockStates), function(fn), solved(false) {
2235+
function(fn), solved(false) {
20572236
// Initialize the entry block as needing an update, and having a partition
20582237
// that places all its non-sendable args in a single region
20592238
blockStates[fn->getEntryBlock()].needsUpdate = true;
@@ -2151,17 +2330,6 @@ class PartitionAnalysis {
21512330
translator.sortUniqueNeverTransferredValues();
21522331
}
21532332

2154-
/// Track the transfer insts that have already had diagnostics emitted about.
2155-
llvm::DenseSet<SILInstruction *> emittedTransferInsts;
2156-
2157-
/// Returns true if a diagnostic has already been emitted for the transferred
2158-
/// instruction \p transferredInst.
2159-
///
2160-
/// If we return false, we insert \p transferredInst into emittedTransferInst.
2161-
bool hasBeenEmitted(SILInstruction *transferredInst) {
2162-
return !emittedTransferInsts.insert(transferredInst).second;
2163-
}
2164-
21652333
/// Once we have reached a fixpoint, this routine runs over all blocks again
21662334
/// reporting any failures by applying our ops to the converged dataflow
21672335
/// state.
@@ -2170,7 +2338,9 @@ class PartitionAnalysis {
21702338

21712339
LLVM_DEBUG(llvm::dbgs() << "Emitting diagnostics for function "
21722340
<< function->getName() << "\n");
2173-
RaceTracer tracer(function, blockStates);
2341+
2342+
SmallFrozenMultiMap<Operand *, SILInstruction *, 8>
2343+
transferOpToRequireInstMultiMap;
21742344

21752345
// Then for each block...
21762346
for (auto [block, blockState] : blockStates) {
@@ -2182,27 +2352,25 @@ class PartitionAnalysis {
21822352
PartitionOpEvaluator eval(workingPartition);
21832353
eval.failureCallback = /*handleFailure=*/
21842354
[&](const PartitionOp &partitionOp, TrackableValueID transferredVal,
2185-
Operand *transferringInst) {
2186-
// Ensure that multiple transfers at the same AST node are only
2187-
// entered once into the race tracer
2188-
if (hasBeenEmitted(partitionOp.getSourceInst()))
2189-
return;
2190-
2191-
LLVM_DEBUG(llvm::dbgs()
2192-
<< " Emitting Use After Transfer Error!\n"
2193-
<< " ID: %%" << transferredVal << "\n"
2194-
<< " Rep: "
2195-
<< *translator.getValueForId(transferredVal)
2196-
->getRepresentative());
2197-
2198-
raceTracer.traceUseOfTransferredValue(partitionOp, transferredVal);
2355+
Operand *transferringOp) {
2356+
auto rep =
2357+
translator.getValueForId(transferredVal)->getRepresentative();
2358+
LLVM_DEBUG(
2359+
llvm::dbgs()
2360+
<< " Emitting Use After Transfer Error!\n"
2361+
<< " ID: %%" << transferredVal << "\n"
2362+
<< " Rep: " << *rep
2363+
<< " Require Inst: " << *partitionOp.getSourceInst()
2364+
<< " Transferring Inst: " << *transferringOp->getUser());
2365+
transferOpToRequireInstMultiMap.insert(transferringOp,
2366+
partitionOp.getSourceInst());
21992367
};
22002368
eval.transferredNonTransferrableCallback =
22012369
[&](const PartitionOp &partitionOp, TrackableValueID transferredVal) {
22022370
LLVM_DEBUG(llvm::dbgs()
2203-
<< "Emitting TransferNonTransferrable Error!\n"
2204-
<< "ID: %%" << transferredVal << "\n"
2205-
<< "Rep: "
2371+
<< " Emitting TransferNonTransferrable Error!\n"
2372+
<< " ID: %%" << transferredVal << "\n"
2373+
<< " Rep: "
22062374
<< *translator.getValueForId(transferredVal)
22072375
->getRepresentative());
22082376
function->getASTContext().Diags.diagnose(
@@ -2223,43 +2391,60 @@ class PartitionAnalysis {
22232391
}
22242392
}
22252393

2226-
// Once we have run over all backs, dump the accumulator and ask the
2227-
// raceTracer to report diagnostics at the transfer sites for all the racy
2228-
// requirement sites entered into it above.
2229-
LLVM_DEBUG(llvm::dbgs() << "Accumulator Complete:\n";
2230-
raceTracer.getAccumulator().print(llvm::dbgs()););
2231-
raceTracer.getAccumulator().emitErrorsForTransferRequire(
2232-
NUM_REQUIREMENTS_TO_DIAGNOSE);
2233-
}
2394+
// Now that we have found all of our transferInsts/Requires emit errors.
2395+
transferOpToRequireInstMultiMap.setFrozen();
2396+
2397+
BasicBlockData<BlockLivenessInfo> blockLivenessInfo(function);
2398+
// We use a generation counter so we can lazily reset blockLivenessInfo
2399+
// since we cannot clear it without iterating over it.
2400+
unsigned blockLivenessInfoGeneration = 0;
2401+
2402+
for (auto [transferOp, requireInsts] :
2403+
transferOpToRequireInstMultiMap.getRange()) {
2404+
2405+
LLVM_DEBUG(llvm::dbgs() << "Transfer Inst: " << *transferOp->getUser());
2406+
2407+
RequireLiveness liveness(blockLivenessInfoGeneration, transferOp,
2408+
blockLivenessInfo);
2409+
++blockLivenessInfoGeneration;
2410+
liveness.process(requireInsts);
2411+
2412+
InferredCallerArgumentTypeInfo argTypeInfo(transferOp);
2413+
auto loc = transferOp->getUser()->getLoc();
2414+
2415+
function->getASTContext()
2416+
.Diags
2417+
.diagnose(loc.getSourceLoc(), diag::call_site_transfer_yields_race,
2418+
argTypeInfo.inferredType,
2419+
argTypeInfo.isolationCrossing.getCallerIsolation(),
2420+
argTypeInfo.isolationCrossing.getCalleeIsolation())
2421+
.highlight(transferOp->get().getLoc().getSourceRange());
2422+
2423+
// Ok, we now have our requires... emit the errors.
2424+
bool didEmitRequire = false;
2425+
2426+
InstructionSet requireInstsUnique(function);
2427+
for (auto *require : requireInsts) {
2428+
// We can have multiple of the same require insts if we had a require
2429+
// and an assign from the same instruction. Our liveness checking above
2430+
// doesn't care about that, but we still need to make sure we do not
2431+
// emit twice.
2432+
if (!requireInstsUnique.insert(require))
2433+
continue;
22342434

2235-
bool tryDiagnoseAsCallSite(const PartitionOp &transferOp,
2236-
unsigned numDisplayed, unsigned numHidden) {
2237-
SILInstruction *sourceInst = transferOp.getSourceInst();
2238-
ApplyExpr *apply = sourceInst->getLoc().getAsASTNode<ApplyExpr>();
2435+
// If this was not a last require, do not emit an error.
2436+
if (!liveness.finalRequires.contains(require))
2437+
continue;
22392438

2240-
// If the transfer does not correspond to an apply expression... return
2241-
// early.
2242-
if (!apply)
2243-
return false;
2439+
auto loc = require->getLoc();
2440+
function->getASTContext()
2441+
.Diags.diagnose(loc.getSourceLoc(), diag::possible_racy_access_site)
2442+
.highlight(loc.getSourceRange());
2443+
didEmitRequire = true;
2444+
}
22442445

2245-
auto isolationCrossing = apply->getIsolationCrossing();
2246-
if (!isolationCrossing) {
2247-
assert(false && "ApplyExprs should be transferring only if"
2248-
" they are isolation crossing");
2249-
return false;
2446+
assert(didEmitRequire && "Should have a require for all errors?!");
22502447
}
2251-
auto argExpr = transferOp.getSourceLoc();
2252-
if (!argExpr)
2253-
assert(false && "sourceExpr should be populated for ApplyExpr transfers");
2254-
2255-
function->getASTContext()
2256-
.Diags
2257-
.diagnose(argExpr.getSourceLoc(), diag::call_site_transfer_yields_race,
2258-
transferOp.getSourceOp()->get()->getType().getASTType(),
2259-
isolationCrossing.value().getCallerIsolation(),
2260-
isolationCrossing.value().getCalleeIsolation())
2261-
.highlight(argExpr.getSourceRange());
2262-
return true;
22632448
}
22642449

22652450
public:

0 commit comments

Comments
 (0)