Skip to content

Commit 8745ab0

Browse files
committed
[rbi] Teach RegionIsolation how to properly error when 'inout sending' params are returned.
We want 'inout sending' parameters to have the semantics that not only are they disconnected on return from the function but additionally they are guaranteed to be in their own disconnected region on return. This implies that we must emit errors when an 'inout sending' parameter or any element that is in the same region as the current value within an 'inout sending' parameter is returned. This commit contains a new diagnostic for RegionIsolation that adds specific logic for detecting and emitting errors in these situations. To implement this, we introduce 3 new diagnostics with each individual diagnostic being slightly different to reflect the various ways that this error can come up in source: * Returning 'inout sending' directly: ```swift func returnInOutSendingDirectly(_ x: inout sending NonSendableKlass) -> NonSendableKlass { return x // expected-warning {{cannot return 'inout sending' parameter 'x' from global function 'returnInOutSendingDirectly'}} // expected-note @-1 {{returning 'x' risks concurrent access since caller assumes that 'x' and the result of global function 'returnInOutSendingDirectly' can be safely sent to different isolation domains}} } ``` * Returning a value in the same region as an 'inout sending' parameter. E.x.: ```swift func returnInOutSendingRegionVar(_ x: inout sending NonSendableKlass) -> NonSendableKlass { var y = x y = x return y // expected-warning {{cannot return 'y' from global function 'returnInOutSendingRegionVar'}} // expected-note @-1 {{returning 'y' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' and the result of global function 'returnInOutSendingRegionVar' can be safely sent to different isolation domains}} } ``` * Returning the result of a function or computed property that is in the same region as the 'inout parameter'. ```swift func returnInOutSendingViaHelper(_ x: inout sending NonSendableKlass) -> NonSendableKlass { let y = x return useNonSendableKlassAndReturn(y) // expected-warning {{cannot return result of global function 'useNonSendableKlassAndReturn' from global function 'returnInOutSendingViaHelper'}} // expected-note @-1 {{returning result of global function 'useNonSendableKlassAndReturn' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' and the result of global function 'returnInOutSendingViaHelper' can be safely sent to different isolation domains}} } ``` Additionally, I had to introduce a specific variant for each of these diagnostics for cases where due to us being in a method, we are actually in our caller causing the 'inout sending' parameter to be in the same region as an actor isolated value: * Returning 'inout sending' directly: ```swift extension MyActor { func returnInOutSendingDirectly(_ x: inout sending NonSendableKlass) -> NonSendableKlass { return x // expected-warning {{cannot return 'inout sending' parameter 'x' from instance method 'returnInOutSendingDirectly'}} // expected-note @-1 {{returning 'x' risks concurrent access since caller assumes that 'x' is not actor-isolated and the result of instance method 'returnInOutSendingDirectly' is 'self'-isolated}} } } ``` * Returning a value in the same region as an 'inout sending' parameter. E.x.: ```swift extension MyActor { func returnInOutSendingRegionLet(_ x: inout sending NonSendableKlass) -> NonSendableKlass { let y = x return y // expected-warning {{cannot return 'y' from instance method 'returnInOutSendingRegionLet'}} // expected-note @-1 {{returning 'y' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' is not actor-isolated and the result of instance method 'returnInOutSendingRegionLet' is 'self'-isolated}} } } ``` * Returning the result of a function or computed property that is in the same region as the 'inout parameter'. ```swift extension MyActor { func returnInOutSendingViaHelper(_ x: inout sending NonSendableKlass) -> NonSendableKlass { let y = x return useNonSendableKlassAndReturn(y) // expected-warning {{cannot return result of global function 'useNonSendableKlassAndReturn' from instance method 'returnInOutSendingViaHelper'; this is an error in the Swift 6 language mode}} // expected-note @-1 {{returning result of global function 'useNonSendableKlassAndReturn' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' is not actor-isolated and the result of instance method 'returnInOutSendingViaHelper' is 'self'-isolated}} } } ``` To implement this, I used two different approaches depending on whether or not the returned value was generic or not. * Concrete In the case where we had a concrete value, I was able to in simple cases emit diagnostics based off of the values returned by the return inst. In cases where we phied together results due to multiple results in the same function, we determine which of the incoming phied values caused the error by grabbing the exit partition information of each of the incoming value predecessors and seeing if an InOutSendingAtFunctionExit would emit an error. * Generic In the case of generic code, it is a little more interesting since the result is a value stored in an our parameter instead of being a value directly returned by a return inst. To work around this, I use PrunedLiveness to determine the last values stored into the out parameter in the function to avoid having to do a full dataflow. Then I take the exit blocks where we assign each of those values and run the same check as we do in the direct phi case to emit the appropriate error. rdar://152454571
1 parent 9f8b989 commit 8745ab0

File tree

11 files changed

+3307
-10
lines changed

11 files changed

+3307
-10
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,32 @@ ERROR(regionbasedisolation_inout_sending_cannot_be_actor_isolated, none,
11191119
NOTE(regionbasedisolation_inout_sending_cannot_be_actor_isolated_note, none,
11201120
"%1 %0 risks causing races in between %1 uses and caller uses since caller assumes value is not actor isolated",
11211121
(Identifier, StringRef))
1122+
ERROR(regionbasedisolation_inout_sending_cannot_be_returned_param, none,
1123+
"'inout sending' parameter %0 cannot be returned",
1124+
(Identifier))
1125+
ERROR(regionbasedisolation_inout_sending_cannot_be_returned_value, none,
1126+
"%0 cannot be returned",
1127+
(Identifier))
1128+
ERROR(regionbasedisolation_inout_sending_cannot_be_returned_value_result, none,
1129+
"result of %kind0 cannot be returned",
1130+
(const ValueDecl *))
1131+
NOTE(regionbasedisolation_inout_sending_cannot_be_returned_note_param, none,
1132+
"returning 'inout sending' parameter %0 risks concurrent access as caller assumes %0 and result can be sent to different isolation domains",
1133+
(Identifier))
1134+
NOTE(regionbasedisolation_inout_sending_cannot_be_returned_note_value, none,
1135+
"returning %0 risks concurrent access to 'inout sending' parameter %1 as caller assumes %1 and result can be sent to different isolation domains",
1136+
(Identifier, Identifier))
1137+
NOTE(regionbasedisolation_inout_sending_cannot_be_returned_note_return_value, none,
1138+
"returning result of %kind0 risks concurrent access to 'inout sending' parameter %1 as caller assumes %1 and result can be sent to different isolation domains",
1139+
(const ValueDecl *, Identifier))
1140+
NOTE(regionbasedisolation_inout_sending_cannot_be_returned_note_actor_param, none,
1141+
"returning %0 risks concurrent access as caller assumes %0 is not actor-isolated and result is %1", (Identifier, StringRef))
1142+
NOTE(regionbasedisolation_inout_sending_cannot_be_returned_note_actor_value, none,
1143+
"returning %0 risks concurrent access to 'inout sending' parameter %1 as caller assumes %1 is not actor-isolated and result is %2",
1144+
(Identifier, Identifier, StringRef))
1145+
NOTE(regionbasedisolation_inout_sending_cannot_be_returned_note_actor_return_value, none,
1146+
"returning result of %kind0 risks concurrent access to 'inout sending' parameter %1 as caller assumes %1 is not actor-isolated and result is %2",
1147+
(const ValueDecl *, Identifier, StringRef))
11221148

11231149
//===
11241150
// Out Sending

include/swift/SILOptimizer/Analysis/RegionAnalysis.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,10 @@ class RegionAnalysisFunctionInfo {
544544

545545
bool isSupportedFunction() const { return supportedFunction; }
546546

547+
NullablePtr<BlockPartitionState> getBlockState(SILBasicBlock *block) const {
548+
return blockStates->get(block);
549+
}
550+
547551
using iterator = BasicBlockData::iterator;
548552
using const_iterator = BasicBlockData::const_iterator;
549553
using reverse_iterator = BasicBlockData::reverse_iterator;

include/swift/SILOptimizer/Utils/PartitionOpError.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ PARTITION_OP_ERROR(InOutSendingNotInitializedAtExit)
5252
/// at end of function without being reinitialized with something disconnected.
5353
PARTITION_OP_ERROR(InOutSendingNotDisconnectedAtExit)
5454

55+
/// This is emitted when a concrete inout sending parameter is returned.
56+
PARTITION_OP_ERROR(InOutSendingReturned)
57+
5558
/// Used to signify an "unknown code pattern" has occured while performing
5659
/// dataflow.
5760
///

include/swift/SILOptimizer/Utils/PartitionUtils.h

Lines changed: 154 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,40 @@ class PartitionOpError {
11261126
}
11271127
};
11281128

1129+
struct InOutSendingReturnedError {
1130+
const PartitionOp *op;
1131+
1132+
/// The 'inout sending' parameter that caused the error to be emitted.
1133+
Element inoutSendingElement;
1134+
1135+
/// The actual returned value. If we return the 'inout sending' parameter
1136+
/// then this will equal inoutSendingElement.
1137+
Element returnedValue;
1138+
1139+
/// If we are actor isolated due to being in the same region as the out
1140+
/// parameter of a actor isolated method... this is that actor isolation.
1141+
SILDynamicMergedIsolationInfo isolationInfo;
1142+
1143+
InOutSendingReturnedError(const PartitionOp &op,
1144+
Element inoutSendingElement,
1145+
Element returnedValue,
1146+
SILDynamicMergedIsolationInfo isolationInfo = {})
1147+
: op(&op), inoutSendingElement(inoutSendingElement),
1148+
returnedValue(returnedValue), isolationInfo(isolationInfo) {}
1149+
1150+
InOutSendingReturnedError(const PartitionOp &op,
1151+
Element inoutSendingElement,
1152+
SILDynamicMergedIsolationInfo isolationInfo = {})
1153+
: InOutSendingReturnedError(op, inoutSendingElement,
1154+
inoutSendingElement, isolationInfo) {}
1155+
1156+
void print(llvm::raw_ostream &os, RegionAnalysisValueMap &valueMap) const;
1157+
1158+
SWIFT_DEBUG_DUMPER(dump(RegionAnalysisValueMap &valueMap)) {
1159+
print(llvm::dbgs(), valueMap);
1160+
}
1161+
};
1162+
11291163
struct NonSendableIsolationCrossingResultError {
11301164
const PartitionOp *op;
11311165

@@ -1297,6 +1331,42 @@ struct PartitionOpEvaluator {
12971331
return SILDynamicMergedIsolationInfo();
12981332
}
12991333

1334+
/// If \p region is not disconnected only because of an out parameter, return
1335+
/// that out parameter. We do not care about other non-disconnected parameters
1336+
/// since we always want to prefer the return error.
1337+
SILValue findNonDisconnectedOutParameterInRegion(Region region) const {
1338+
for (const auto &pair : p.range()) {
1339+
if (pair.second != region)
1340+
continue;
1341+
auto info = getIsolationRegionInfo(pair.first);
1342+
1343+
// If we do not have any isolation info, then something bad
1344+
// happened. Return SILValue().
1345+
if (!info)
1346+
return {};
1347+
1348+
// If we have something that is disconnected... continue. We do not care
1349+
// about this.
1350+
if (info.isDisconnected())
1351+
continue;
1352+
1353+
// See if we ahve an indirect out parameter...
1354+
if (auto value = asImpl().getIndirectOutParameter(pair.first)) {
1355+
return value;
1356+
}
1357+
1358+
// If this was not an out parameter, return {}.
1359+
return {};
1360+
}
1361+
1362+
// After going over our loop, if result is not SILValue(), then we found
1363+
// that our region is not disconnected due only to a single 'indirect out'
1364+
// parameter. If SILValue() then we had all disconnected values. If we found
1365+
// multiple non-disconnected things, we would have bailed earlier in the
1366+
// loop itself.
1367+
return SILValue();
1368+
}
1369+
13001370
bool isTaskIsolatedDerived(Element elt) const {
13011371
return asImpl().isTaskIsolatedDerived(elt);
13021372
}
@@ -1534,7 +1604,9 @@ struct PartitionOpEvaluator {
15341604
"Require PartitionOp's argument should already be tracked");
15351605

15361606
// First check if the region of our 'inout sending' element has been
1537-
// sent. In that case, we emit a special use after free error.
1607+
// sent. In that case, we emit a special use after free error so that the
1608+
// user knows that they need to reinitialize the 'inout sending'
1609+
// parameter.
15381610
if (auto *sentOperandSet = p.getSentOperandSet(op.getOpArg1())) {
15391611
for (auto sentOperand : sentOperandSet->data()) {
15401612
handleError(InOutSendingNotInitializedAtExitError(op, op.getOpArg1(),
@@ -1543,7 +1615,7 @@ struct PartitionOpEvaluator {
15431615
return;
15441616
}
15451617

1546-
// If we were not sent, check if our region is actor isolated. If so,
1618+
// If we were not sent, check if our region is not disconnected. If so,
15471619
// error since we need a disconnected value in the inout parameter.
15481620
Region inoutSendingRegion = p.getRegion(op.getOpArg1());
15491621
auto dynamicRegionIsolation = getIsolationRegionInfo(inoutSendingRegion);
@@ -1554,12 +1626,82 @@ struct PartitionOpEvaluator {
15541626
return;
15551627
}
15561628

1557-
// Otherwise, emit the error if the dynamic region isolation is not
1558-
// disconnected.
1629+
auto handleDirectReturn = [&]() -> bool {
1630+
bool emittedDiagnostic = false;
1631+
1632+
// For each value we are returning...
1633+
for (auto value : op.getSourceInst()->getOperandValues()) {
1634+
// Find its element and check if that element is in the same region as
1635+
// our inout sending param...
1636+
auto elt = asImpl().getElement(value);
1637+
if (!elt || inoutSendingRegion != p.getRegion(*elt))
1638+
continue;
1639+
1640+
emittedDiagnostic = true;
1641+
// Then check if our element is the same as our inoutSendingParam. In
1642+
// that case, we emit a special error that only refers to the
1643+
// inoutSendingParam as one entity.
1644+
//
1645+
// NOTE: This is taking advantage of us looking through loads when
1646+
// determining element identity. When that changes, this code will
1647+
// need to be updated to look through loads.
1648+
if (*elt == op.getOpArg1()) {
1649+
handleError(InOutSendingReturnedError(op, op.getOpArg1(),
1650+
dynamicRegionIsolation));
1651+
continue;
1652+
}
1653+
1654+
// Otherwise, we need to refer to a different value in the same region
1655+
// as the 'inout sending' parameter. Emit a special error. For that.
1656+
handleError(InOutSendingReturnedError(op, op.getOpArg1(), *elt,
1657+
dynamicRegionIsolation));
1658+
}
1659+
1660+
return emittedDiagnostic;
1661+
};
1662+
1663+
// Then check if our region isolated value is not disconnected...
15591664
if (!dynamicRegionIsolation.isDisconnected()) {
1665+
// Before we emit the more general "inout sending" not disconnected at
1666+
// exit error... check if our dynamic region isolation is not
1667+
// disconnected since it is in the same region an out parameter. In that
1668+
// case we want to emit a special 'cannot return value' error.
1669+
if (auto outParam =
1670+
findNonDisconnectedOutParameterInRegion(inoutSendingRegion)) {
1671+
handleError(InOutSendingReturnedError(op, op.getOpArg1(),
1672+
getElement(outParam).value(),
1673+
dynamicRegionIsolation));
1674+
return;
1675+
}
1676+
1677+
// If we did not have an out parameter, see if we are returning a
1678+
// value that is in the region as our 'inout sending' parameter.
1679+
if (handleDirectReturn())
1680+
return;
1681+
1682+
// Otherwise, we emit the normal not disconnected at exit error.
15601683
handleError(InOutSendingNotDisconnectedAtExitError(
15611684
op, op.getOpArg1(), dynamicRegionIsolation));
1685+
return;
15621686
}
1687+
1688+
// Ok, we have a disconnected value. We could still be returning a
1689+
// disconnected value in the same region as an 'inout sending'
1690+
// parameter. We cannot allow that since the caller considers 'inout
1691+
// sending' values to be in their own region on function return. So it
1692+
// would assume that it could potentially send the value in the 'inout
1693+
// sending' parameter and the return value to different isolation
1694+
// domains... allowing for races.
1695+
1696+
// Even though we are disconnected, see if our SILFunction has an actor
1697+
// isolation. In that case, we want to use that since the direct return
1698+
// value will be inferred in the caller to have that isolation.
1699+
if (auto isolation = SILIsolationInfo::getFunctionIsolation(
1700+
op.getSourceInst()->getFunction())) {
1701+
dynamicRegionIsolation = {isolation};
1702+
}
1703+
1704+
handleDirectReturn();
15631705
return;
15641706
}
15651707
case PartitionOpKind::UnknownPatternError:
@@ -1569,10 +1711,6 @@ struct PartitionOpEvaluator {
15691711
// Then emit an unknown code pattern error.
15701712
return handleError(UnknownCodePatternError(op));
15711713
case PartitionOpKind::NonSendableIsolationCrossingResult: {
1572-
// Grab the dynamic dataflow isolation information for our element's
1573-
// region.
1574-
Region region = p.getRegion(op.getOpArg1());
1575-
15761714
// Then emit the error.
15771715
return handleError(
15781716
NonSendableIsolationCrossingResultError(op, op.getOpArg1()));
@@ -1757,6 +1895,14 @@ struct PartitionOpEvaluatorBaseImpl : PartitionOpEvaluator<Subclass> {
17571895
/// isolated value.
17581896
bool isTaskIsolatedDerived(Element elt) const { return false; }
17591897

1898+
/// If \p elt maps to a representative that is an indirect out parameter,
1899+
/// return that value.
1900+
SILValue getIndirectOutParameter(Element elt) const { return {}; }
1901+
1902+
/// If \p elt maps to a representative that is an 'inout sending' parameter
1903+
/// that returns that value.
1904+
SILValue getInOutSendingParameter(Element elt) const { return {}; }
1905+
17601906
/// By default, do nothing upon error.
17611907
void handleError(PartitionOpError error) {}
17621908

include/swift/SILOptimizer/Utils/SILIsolationInfo.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,11 @@ class SILIsolationInfo {
546546
/// Infer isolation of conformances for the given instruction.
547547
static SILIsolationInfo getConformanceIsolation(SILInstruction *inst);
548548

549+
/// Return SILIsolationInfo based off of the attached ActorIsolation of \p
550+
/// fn. If \p fn does not have an actor isolation set, returns an invalid
551+
/// SILIsolationInfo.
552+
static SILIsolationInfo getFunctionIsolation(SILFunction *fn);
553+
549554
/// A helper that is used to ensure that we treat certain builtin values as
550555
/// non-Sendable that the AST level otherwise thinks are non-Sendable.
551556
///

lib/SILOptimizer/Analysis/RegionAnalysis.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4115,7 +4115,10 @@ bool BlockPartitionState::recomputeExitFromEntry(
41154115
}
41164116

41174117
std::optional<Element> getElement(SILValue value) const {
4118-
return translator.getValueMap().getTrackableValue(value).value.getID();
4118+
auto trackableValue = translator.getValueMap().getTrackableValue(value);
4119+
if (trackableValue.value.isSendable())
4120+
return {};
4121+
return trackableValue.value.getID();
41194122
}
41204123

41214124
SILValue getRepresentative(SILValue value) const {

0 commit comments

Comments
 (0)