Skip to content

Commit fe238cf

Browse files
authored
Merge pull request #75162 from atrick/inout-dep
LifetimeDependenceDiagnostics support for inout reassignment.
2 parents 95cff2a + 11ba799 commit fe238cf

13 files changed

+629
-146
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,37 @@ private struct DiagnoseDependence {
131131
reportEscaping(operand: operand)
132132
}
133133

134+
func checkInoutResult(argument inoutArg: FunctionArgument) -> WalkResult {
135+
// Check that the parameter dependence for this inout argument is the same as the current dependence scope.
136+
if let sourceArg = dependence.scope.parentValue as? FunctionArgument {
137+
// If the inout result is also the inout source, then it's always ok.
138+
if inoutArg == sourceArg {
139+
return .continueWalk
140+
}
141+
if function.argumentConventions.getDependence(target: inoutArg.index, source: sourceArg.index) != nil {
142+
// The inout result depends on a lifetime that is inherited or borrowed in the caller.
143+
log(" has dependent inout argument: \(inoutArg)")
144+
return .continueWalk
145+
}
146+
}
147+
return .abortWalk
148+
}
149+
150+
func checkStoreToYield(address: Value) -> WalkResult {
151+
var walker = DependentAddressUseDefWalker(context: context, diagnostics: self)
152+
return walker.walkUp(address: address)
153+
}
154+
155+
func checkYield(operand: Operand) -> WalkResult {
156+
switch dependence.scope {
157+
case .caller:
158+
return checkFunctionResult(operand: operand)
159+
default:
160+
// local scopes can be yielded without escaping.
161+
return .continueWalk
162+
}
163+
}
164+
134165
func checkFunctionResult(operand: Operand) -> WalkResult {
135166

136167
if function.hasUnsafeNonEscapableResult {
@@ -151,16 +182,16 @@ private struct DiagnoseDependence {
151182
if dependence.isUnsafeApplyResult, function.hasResultDependence {
152183
return .continueWalk
153184
}
154-
// Check that the argument dependence for this result is the same
185+
// Check that the parameter dependence for this result is the same
155186
// as the current dependence scope.
156187
if let arg = dependence.scope.parentValue as? FunctionArgument,
157188
function.argumentConventions[resultDependsOn: arg.index] != nil {
158189
// The returned value depends on a lifetime that is inherited or
159190
// borrowed in the caller. The lifetime of the argument value
160191
// itself is irrelevant here.
192+
log(" has dependent function result")
161193
return .continueWalk
162194
}
163-
reportEscaping(operand: operand)
164195
return .abortWalk
165196
}
166197

@@ -312,6 +343,50 @@ private struct LifetimeVariable {
312343
}
313344
}
314345

346+
/// Walk up an address into which a dependent value has been stored. If any address in the use-def chain is a
347+
/// mark_dependence, follow the depenedence base rather than the forwarded value. If any of the dependence bases in
348+
/// within the current scope is with (either local checkInoutResult), then storing a value into that address is
349+
/// nonescaping.
350+
///
351+
/// This supports store-to-yield. Storing to a yield is an escape unless the yielded memory location depends on another
352+
/// lifetime that already depends on the current scope. When setter depends on 'newValue', 'newValue' is stored to the
353+
/// yielded address, and the yielded addrses depends on the lifetime of 'self'. A mark_dependence should have already
354+
/// been inserted for that lifetime depenence:
355+
///
356+
/// (%a, %t) = begin_apply %f(%self)
357+
/// : $@yield_once @convention(method) (@inout Self) -> _inherit(0) @yields @inout Self.field
358+
/// %dep = mark_dependence [nonescaping] %yield_addr on %self
359+
/// store %newValue to [assign] %dep : $*Self.field
360+
///
361+
private struct DependentAddressUseDefWalker {
362+
let context: Context
363+
var diagnostics: DiagnoseDependence
364+
}
365+
366+
extension DependentAddressUseDefWalker: AddressUseDefWalker {
367+
// Follow the dependence base, not the forwarded value. Similar to the way LifetimeDependenceUseDefWalker handles
368+
// MarkDependenceInst.
369+
mutating func walkUp(address: Value, path: UnusedWalkingPath = UnusedWalkingPath()) -> WalkResult {
370+
if let markDep = address as? MarkDependenceInst, let addressDep = LifetimeDependence(markDep, context) {
371+
switch addressDep.scope {
372+
case let .caller(arg):
373+
return diagnostics.checkInoutResult(argument: arg)
374+
case .owned, .initialized:
375+
// Storing a nonescaping value to local memory cannot escape.
376+
return .abortWalk
377+
default:
378+
break
379+
}
380+
}
381+
return walkUpDefault(address: address, path: UnusedWalkingPath())
382+
}
383+
384+
mutating func rootDef(address: Value, path: UnusedWalkingPath) -> WalkResult {
385+
// This only searches for mark_dependence scopes.
386+
return .continueWalk
387+
}
388+
}
389+
315390
/// Walk down lifetime depenence uses. For each check that all dependent
316391
/// leaf uses are non-escaping and within the dependence scope. The walk
317392
/// starts with add address for .access dependencies. The walk can
@@ -363,17 +438,45 @@ extension DiagnoseDependenceWalker : LifetimeDependenceDefUseWalker {
363438
return .abortWalk
364439
}
365440

441+
mutating func inoutDependence(argument: FunctionArgument, on operand: Operand) -> WalkResult {
442+
if diagnostics.checkInoutResult(argument: argument) == .abortWalk {
443+
diagnostics.reportEscaping(operand: operand)
444+
return .abortWalk
445+
}
446+
return .continueWalk
447+
}
448+
366449
mutating func returnedDependence(result: Operand) -> WalkResult {
367-
return diagnostics.checkFunctionResult(operand: result)
450+
if diagnostics.checkFunctionResult(operand: result) == .abortWalk {
451+
diagnostics.reportEscaping(operand: result)
452+
return .abortWalk
453+
}
454+
return .continueWalk
368455
}
369456

370457
mutating func returnedDependence(address: FunctionArgument,
371-
using operand: Operand) -> WalkResult {
372-
return diagnostics.checkFunctionResult(operand: operand)
458+
on operand: Operand) -> WalkResult {
459+
if diagnostics.checkFunctionResult(operand: operand) == .abortWalk {
460+
diagnostics.reportEscaping(operand: operand)
461+
return .abortWalk
462+
}
463+
return .continueWalk
373464
}
374465

375466
mutating func yieldedDependence(result: Operand) -> WalkResult {
376-
return diagnostics.checkFunctionResult(operand: result)
467+
if diagnostics.checkYield(operand: result) == .abortWalk {
468+
diagnostics.reportEscaping(operand: result)
469+
return .abortWalk
470+
}
471+
return .continueWalk
472+
}
473+
474+
mutating func storeToYieldDependence(address: Value, of operand: Operand) -> WalkResult {
475+
if diagnostics.checkStoreToYield(address: address) == .abortWalk {
476+
diagnostics.reportEscaping(operand: operand)
477+
return .abortWalk
478+
}
479+
return .continueWalk
377480
}
378481

379482
// Override AddressUseVisitor here because LifetimeDependenceDefUseWalker

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceInsertion.swift

Lines changed: 132 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private let verbose = false
2424

2525
private func log(prefix: Bool = true, _ message: @autoclosure () -> String) {
2626
if verbose {
27-
print((prefix ? "### " : "") + message())
27+
debugLog(prefix: prefix, message())
2828
}
2929
}
3030

@@ -40,7 +40,10 @@ let lifetimeDependenceInsertionPass = FunctionPass(
4040

4141
for instruction in function.instructions {
4242
if let dependentApply = LifetimeDependentApply(instruction) {
43-
insertDependencies(for: dependentApply, context)
43+
for operand in dependentApply.applySite.parameterOperands {
44+
insertParameterDependencies(apply: dependentApply, target: operand, context)
45+
}
46+
insertResultDependencies(for: dependentApply, context)
4447
}
4548
}
4649
}
@@ -53,7 +56,7 @@ private struct LifetimeDependentApply {
5356
guard let apply = instruction as? FullApplySite else {
5457
return nil
5558
}
56-
if !apply.hasResultDependence {
59+
if !apply.hasLifetimeDependence {
5760
return nil
5861
}
5962
self.applySite = apply
@@ -85,50 +88,145 @@ private struct LifetimeDependentApply {
8588
}
8689

8790
extension LifetimeDependentApply {
88-
/// A lifetime argument copies, borrows, or mutatably borrows the
89-
/// lifetime of the argument value.
90-
struct LifetimeArgument {
91+
enum TargetKind {
92+
case result
93+
case inParameter
94+
case inoutParameter
95+
case yield
96+
case yieldAddress
97+
}
98+
99+
/// A lifetime argument that either inherits or creates a new scope for the lifetime of the argument value.
100+
struct LifetimeSource {
91101
let convention: LifetimeDependenceConvention
92102
let value: Value
93103
}
94104

95-
func getLifetimeArguments() -> SingleInlineArray<LifetimeArgument> {
96-
var args = SingleInlineArray<LifetimeArgument>()
105+
/// List of lifetime dependencies for a single target.
106+
struct LifetimeSources {
107+
let targetKind: TargetKind
108+
var sources = SingleInlineArray<LifetimeSource>()
109+
}
110+
111+
func getResultDependenceSources() -> LifetimeSources? {
112+
guard applySite.hasResultDependence else { return nil }
113+
var sources: LifetimeSources
114+
switch applySite {
115+
case let beginApply as BeginApplyInst:
116+
if beginApply.yieldedValues.contains(where: { $0.type.isAddress }) {
117+
sources = LifetimeSources(targetKind: .yieldAddress)
118+
} else {
119+
sources = LifetimeSources(targetKind: .yield)
120+
}
121+
default:
122+
sources = LifetimeSources(targetKind: .result)
123+
}
97124
for operand in applySite.parameterOperands {
98125
guard let dep = applySite.resultDependence(on: operand) else {
99126
continue
100127
}
101-
args.push(LifetimeArgument(convention: dep, value: operand.value))
128+
sources.sources.push(LifetimeSource(convention: dep, value: operand.value))
129+
}
130+
return sources
131+
}
132+
133+
func getParameterDependenceSources(target: Operand) -> LifetimeSources? {
134+
guard let deps = applySite.parameterDependencies(target: target) else {
135+
return nil
136+
}
137+
var sources: LifetimeSources
138+
let convention = applySite.convention(of: target)!
139+
switch convention {
140+
case .indirectInout, .indirectInoutAliasable, .packInout:
141+
sources = LifetimeSources(targetKind: .inoutParameter)
142+
case .indirectIn, .indirectInGuaranteed, .indirectInCXX, .directOwned, .directUnowned, .directGuaranteed,
143+
.packOwned, .packGuaranteed:
144+
sources = LifetimeSources(targetKind: .inParameter)
145+
case .indirectOut, .packOut:
146+
debugLog("\(applySite)")
147+
fatalError("Lifetime dependencies cannot target \(convention) parameter")
148+
}
149+
for (dep, operand) in zip(deps, applySite.parameterOperands) {
150+
guard let dep = dep else {
151+
continue
152+
}
153+
sources.sources.push(LifetimeSource(convention: dep, value: operand.value))
154+
}
155+
return sources
156+
}
157+
158+
// Scoped dependencies require a mark_dependence for every variable that introduces this scope.
159+
//
160+
// Inherited dependencies do not require a mark_dependence if the target is a result or yielded value. The inherited
161+
// lifetime is nonescapable, so either
162+
//
163+
// (a) the result or yield is never returned from this function
164+
//
165+
// (b) the inherited lifetime has a dependence root within this function (it comes from a dependent function argument
166+
// or scoped dependence). In this case, when that depedence root is diagnosed, the analysis will find transtive uses
167+
// of this apply's result.
168+
//
169+
// (c) the dependent value is passed to another call with a dependent inout argument, or it is stored to a yielded
170+
// address of a coroutine that has a dependent inout argument. In this case, a mark_dependence will already be created
171+
// for that inout argument.
172+
//
173+
// Parameter dependencies and yielded addresses always require a mark_dependence.
174+
static func findDependenceBases(sources: LifetimeSources, _ context: FunctionPassContext) -> [Value] {
175+
var bases: [Value] = []
176+
for source in sources.sources {
177+
switch source.convention {
178+
case .inherit:
179+
switch sources.targetKind {
180+
case .result, .yield:
181+
continue
182+
case .inParameter, .inoutParameter, .yieldAddress:
183+
_ = LifetimeDependence.visitDependenceRoots(enclosing: source.value, context) { scope in
184+
log("Inherited lifetime from \(source.value)")
185+
log(" scope: \(scope)")
186+
bases.append(scope.parentValue)
187+
return .continueWalk
188+
}
189+
}
190+
case .scope:
191+
// Create a new dependence on the apply's access to the argument.
192+
for varIntoducer in gatherVariableIntroducers(for: source.value, context) {
193+
if let scope = LifetimeDependence.Scope(base: varIntoducer, context) {
194+
log("Scoped lifetime from \(source.value)")
195+
log(" scope: \(scope)")
196+
bases.append(scope.parentValue)
197+
}
198+
}
199+
}
102200
}
103-
return args
201+
return bases
104202
}
105203
}
106204

107205
/// If the result of this apply depends on the scope of one or more
108206
/// arguments, then insert a mark_dependence [unresolved] from the
109207
/// result on each argument so that the result is recognized as a
110208
/// dependent value within each scope.
111-
private func insertDependencies(for apply: LifetimeDependentApply,
112-
_ context: FunctionPassContext ) {
113-
let bases = findDependenceBases(of: apply, context)
209+
private func insertResultDependencies(for apply: LifetimeDependentApply, _ context: FunctionPassContext ) {
210+
guard let sources = apply.getResultDependenceSources() else {
211+
return
212+
}
213+
log("Creating dependencies for \(apply.applySite)")
214+
215+
let bases = LifetimeDependentApply.findDependenceBases(sources: sources, context)
216+
114217
for dependentValue in apply.applySite.resultOrYields {
115218
let builder = Builder(before: dependentValue.nextInstruction, context)
116-
insertMarkDependencies(value: dependentValue, initializer: nil,
117-
bases: bases, builder: builder, context)
219+
insertMarkDependencies(value: dependentValue, initializer: nil, bases: bases, builder: builder, context)
118220
}
119221
for resultOper in apply.applySite.indirectResultOperands {
120222
let accessBase = resultOper.value.accessBase
121-
guard let (initialAddress, initializingStore) =
122-
accessBase.findSingleInitializer(context) else {
223+
guard let (initialAddress, initializingStore) = accessBase.findSingleInitializer(context) else {
123224
continue
124225
}
125-
// TODO: This is currently too strict for a diagnostic pass. We
126-
// should handle/cleanup projections and casts that occur before
127-
// the initializingStore. Or check in the SIL verifier that all
128-
// stores without an access scope follow this form. Then convert
129-
// this bail-out to an assert.
130-
guard initialAddress.usesOccurOnOrAfter(instruction: initializingStore,
131-
context) else {
226+
// TODO: This might bail-out on SIL that should be diagnosed. We should handle/cleanup projections and casts that
227+
// occur before the initializingStore. Or check in the SIL verifier that all stores without an access scope follow
228+
// this form. Then convert this bail-out to an assert.
229+
guard initialAddress.usesOccurOnOrAfter(instruction: initializingStore, context) else {
132230
continue
133231
}
134232
assert(initializingStore == resultOper.instruction, "an indirect result is a store")
@@ -139,29 +237,18 @@ private func insertDependencies(for apply: LifetimeDependentApply,
139237
}
140238
}
141239

142-
private func findDependenceBases(of apply: LifetimeDependentApply,
143-
_ context: FunctionPassContext)
144-
-> [Value] {
240+
private func insertParameterDependencies(apply: LifetimeDependentApply, target: Operand,
241+
_ context: FunctionPassContext ) {
242+
guard let sources = apply.getParameterDependenceSources(target: target) else {
243+
return
244+
}
145245
log("Creating dependencies for \(apply.applySite)")
146-
var bases: [Value] = []
147-
for lifetimeArg in apply.getLifetimeArguments() {
148-
switch lifetimeArg.convention {
149-
case .inherit:
150-
continue
151-
case .scope:
152-
// Create a new dependence on the apply's access to the argument.
153-
for varIntoducer in gatherVariableIntroducers(for: lifetimeArg.value,
154-
context) {
155-
if let scope =
156-
LifetimeDependence.Scope(base: varIntoducer, context) {
157-
log("Scoped lifetime from \(lifetimeArg.value)")
158-
log(" scope: \(scope)")
159-
bases.append(scope.parentValue)
160-
}
161-
}
162-
}
246+
247+
let bases = LifetimeDependentApply.findDependenceBases(sources: sources, context)
248+
249+
Builder.insert(after: apply.applySite, context) {
250+
insertMarkDependencies(value: target.value, initializer: nil, bases: bases, builder: $0, context)
163251
}
164-
return bases
165252
}
166253

167254
private func insertMarkDependencies(value: Value, initializer: Instruction?,

0 commit comments

Comments
 (0)