Skip to content

Commit b47bd15

Browse files
authored
Merge pull request #62668 from atrick/fix-recursive-access
Do not insert dynamic exclusivity checks in closure cycles
2 parents db21566 + d593623 commit b47bd15

File tree

3 files changed

+84
-8
lines changed

3 files changed

+84
-8
lines changed

lib/SILOptimizer/Mandatory/AccessEnforcementSelection.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ raw_ostream &operator<<(raw_ostream &os, const AddressCapture &capture) {
9999

100100
// For each non-escaping closure, record the indices of arguments that
101101
// require dynamic enforcement.
102+
//
103+
// A note on closure cycles: local functions can be recursive, creating closure
104+
// cycles. DynamicCaptures ignores such cycles, simply processing the call graph
105+
// top-down. This relies on a simple rule: if a captured variable is passed as a
106+
// box a local function (presumably because the function escapes), then it must
107+
// also be passed as a box to any other local function called by the
108+
// first. Therefore, if any capture escapes in a closure cycle, then it must be
109+
// passed as a box in all closures within the cycle. DynamicCaptures does not
110+
// care about boxes, because they are always dynamically enforced.
102111
class DynamicCaptures {
103112
const ClosureFunctionOrder &closureOrder;
104113

@@ -127,10 +136,10 @@ class DynamicCaptures {
127136
}
128137

129138
bool isDynamic(SILFunctionArgument *arg) const {
130-
// If the current function is a local function that directly or indirectly
131-
// refers to itself, then conservatively assume dynamic enforcement.
132-
if (closureOrder.isHeadOfClosureCycle(arg->getFunction()))
133-
return true;
139+
// This closure may be the head of a closure cycle. That's ok, because we
140+
// only care about whether this argument escapes in the calling function
141+
// this is *not* part of the cycle. If the capture escapes anywhere in the
142+
// cycle, then it is passed as a box to all closures in that cycle.
134143

135144
auto pos = dynamicCaptureMap.find(arg->getFunction());
136145
if (pos == dynamicCaptureMap.end())

test/Interpreter/enforce_exclusive_access.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,56 @@ ExclusiveAccessTestSuite.test("directlyAppliedEscapingConflict") {
383383
}()
384384
}
385385

386+
// rdar://102056143 - recursive locals inside a mutating method
387+
// effectively capture self as inout even if they don't modify
388+
// self. This should not trigger a runtime trap.
389+
struct RecursiveLocalDuringMutation {
390+
var flag: Bool = false
391+
392+
mutating func update() {
393+
func local1(_ done: Bool) -> Bool {
394+
if !done {
395+
return local2()
396+
}
397+
return flag
398+
}
399+
func local2() -> Bool {
400+
return local1(true)
401+
}
402+
flag = !local1(false)
403+
}
404+
}
405+
406+
ExclusiveAccessTestSuite.test("recursiveLocalDuringMutation") {
407+
var v = RecursiveLocalDuringMutation()
408+
v.update()
409+
_blackHole(v.flag)
410+
}
411+
412+
// rdar://102056143 - negative test. Make sure we still catch true
413+
// violations on recursive captures.
414+
struct RecursiveCaptureViolation {
415+
func testBadness() -> Bool {
416+
var flag = false
417+
func local1(_ x: inout Bool) {
418+
if !flag {
419+
x = true
420+
local2()
421+
}
422+
}
423+
func local2() {
424+
local1(&flag)
425+
}
426+
local2()
427+
return flag
428+
}
429+
}
430+
431+
ExclusiveAccessTestSuite.test("recursiveCaptureViolation") {
432+
var s = RecursiveCaptureViolation()
433+
expectCrashLater()
434+
s.testBadness()
435+
_blackHole(s)
436+
}
437+
386438
runAllTests()

test/SILOptimizer/access_enforcement_selection.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,16 @@ public func recaptureStack() -> Int {
101101

102102

103103
// -----------------------------------------------------------------------------
104-
// Inout access in a recursive closure currently has dynamic enforcement
105-
// because enforcement selection cannot visit all closure scopes before
106-
// selecting enforcement within the closure.
104+
// Inout access in a recursive non-escaping closure currently has
105+
// static enforcement.
107106

108107
public protocol Apply {
109108
func apply(_ body: (Apply) -> ())
110109
}
111110

112111
// CHECK-LABEL: sil private @$s28access_enforcement_selection20testRecursiveClosure1a1xyAA5Apply_p_SiztF9localFuncL_1byAaE_p_tF : $@convention(thin) (@in_guaranteed any Apply, @inout_aliasable Int) -> () {
113112
// CHECK: bb0(%0 : $*any Apply, %1 : @closureCapture $*Int):
114-
// CHECK: begin_access [modify] [dynamic] %1 : $*Int
113+
// CHECK: begin_access [modify] [static] %1 : $*Int
115114
// CHECK-LABEL: } // end sil function '$s28access_enforcement_selection20testRecursiveClosure1a1xyAA5Apply_p_SiztF9localFuncL_1byAaE_p_tF'
116115
public func testRecursiveClosure(a: Apply, x: inout Int) {
117116
func localFunc(b: Apply) {
@@ -123,3 +122,19 @@ public func testRecursiveClosure(a: Apply, x: inout Int) {
123122
}
124123
a.apply(localFunc)
125124
}
125+
126+
// CHECK-LABEL: sil private @$s28access_enforcement_selection25testRecursiveLocalCapture1iys5Int64Vz_tF6local2L_yyF : $@convention(thin) (@inout_aliasable Int64) -> () {
127+
// CHECK: begin_access [modify] [static] %0 : $*Int64
128+
// CHECK-LABEL: } // end sil function '$s28access_enforcement_selection25testRecursiveLocalCapture1iys5Int64Vz_tF6local2L_yyF'
129+
func testRecursiveLocalCapture(i: inout Int64) {
130+
func local1() {
131+
if i < 1 {
132+
local2()
133+
}
134+
}
135+
func local2() {
136+
i = i + 1
137+
local1()
138+
}
139+
local1()
140+
}

0 commit comments

Comments
 (0)