Repository: swiftlang/swift
Pull request #79810: Fix copy-to-borrow optimization's end_borrow insertion
Author: login: meg-gupta
Created: 2025-03-06T00:38:33Z | Merged: 2025-03-07T12:00:17Z

allLifetimeEndingInstructions collects insertion points for end_borrows, and can end up having duplicate entries in the case of enums with non payloaded cases.

Unique the entries so we don't end up with multiple end_borrows

Fixes rdar://146212574




Diff:
diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
index 1b77450e05881..b9caf36d8b892 100644
--- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
+++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
@@ -124,10 +124,6 @@ private struct Uses {
   // E.g. the none-case of a switch_enum of an Optional.
   private(set) var nonDestroyingLiverangeExits: Stack<Instruction>
 
-  var allLifetimeEndingInstructions: [Instruction] {
-    Array(destroys.lazy.map { $0 }) + Array(nonDestroyingLiverangeExits)
-  }
-
   private(set) var usersInDeadEndBlocks: Stack<Instruction>
 
   init(_ context: FunctionPassContext) {
@@ -323,7 +319,16 @@ private func createEndBorrows(for beginBorrow: Value, atEndOf liverange: Instruc
   //   destroy_value %2
   //   destroy_value %3  // The final destroy. Here we need to create the `end_borrow`(s)
   //
-  for endInst in collectedUses.allLifetimeEndingInstructions {
+
+  var allLifetimeEndingInstructions = InstructionWorklist(context)
+  allLifetimeEndingInstructions.pushIfNotVisited(contentsOf: collectedUses.destroys.lazy.map { $0 })
+  allLifetimeEndingInstructions.pushIfNotVisited(contentsOf: collectedUses.nonDestroyingLiverangeExits)
+
+  defer {
+    allLifetimeEndingInstructions.deinitialize()
+  }
+
+  while let endInst = allLifetimeEndingInstructions.pop() {
     if !liverange.contains(endInst) {
       let builder = Builder(before: endInst, context)
       builder.createEndBorrow(of: beginBorrow)
diff --git a/test/SILOptimizer/copy-to-borrow-optimization.sil b/test/SILOptimizer/copy-to-borrow-optimization.sil
index 5e8cb4342bd46..845324ca7f615 100644
--- a/test/SILOptimizer/copy-to-borrow-optimization.sil
+++ b/test/SILOptimizer/copy-to-borrow-optimization.sil
@@ -2156,3 +2156,49 @@ bb0(%0 : $*MultiPayload):
   return %2
 }
 
+// CHECK-LABEL: sil [ossa] @duplicate_lifetime_end1 :
+// CHECK:         load_borrow %0
+// CHECK:       } // end sil function 'duplicate_lifetime_end1'
+sil [ossa] @duplicate_lifetime_end1 : $@convention(thin) (@in_guaranteed (C, Optional<C>)) -> () {
+bb0(%0 : $*(C, Optional<C>)):
+  %1 = load [copy] %0
+  (%2, %3) = destructure_tuple %1
+  switch_enum %3, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2
+
+bb1(%4 : @owned $C):
+  destroy_value %2
+  destroy_value %4
+  br bb3
+
+bb2:
+  debug_value %2
+  destroy_value %2
+  br bb3
+
+bb3:
+  %r = tuple ()
+  return %r
+}
+
+// CHECK-LABEL: sil [ossa] @duplicate_lifetime_end2 :
+// CHECK:         load_borrow %0
+// CHECK:       } // end sil function 'duplicate_lifetime_end2'
+sil [ossa] @duplicate_lifetime_end2 : $@convention(thin) (@in_guaranteed (C, Optional<C>)) -> () {
+bb0(%0 : $*(C, Optional<C>)):
+  %1 = load [copy] %0
+  (%2, %3) = destructure_tuple %1
+  switch_enum %3, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2
+
+bb1(%4 : @owned $C):
+  destroy_value %2
+  destroy_value %4
+  br bb3
+
+bb2:
+  destroy_value %2
+  br bb3
+
+bb3:
+  %r = tuple ()
+  return %r
+}


Reviews:
user: login: nate-chandler
body: lgtm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T00:41:02Z
user: login: eeckstein
body: Thanks for fixing this!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T08:09:44Z
user: login: meg-gupta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:57:32Z
user: login: meg-gupta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:57:40Z
user: login: eeckstein
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T20:15:27Z

Review comments:
diff_hunk: @@ -124,8 +124,11 @@ private struct Uses {
   // E.g. the none-case of a switch_enum of an Optional.
   private(set) var nonDestroyingLiverangeExits: Stack<Instruction>
 
-  var allLifetimeEndingInstructions: [Instruction] {
-    Array(destroys.lazy.map { $0 }) + Array(nonDestroyingLiverangeExits)
+  // FIXME: Use an InstructionSetVector
+  var allLifetimeEndingInstructions : InstructionWorklist {
+    var worklist = InstructionWorklist(context)
+    worklist.pushIfNotVisited(contentsOf: Array(destroys.lazy.map { $0 }) + Array(nonDestroyingLiverangeExits))
path: SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
user: login: eeckstein
body: You don't need to "convert" the lazy map sequence to an array, because `pushIfNotVisited` accepts any kind of sequence.
Instead you can call `pushIfNotVisited` twice:
```
  worklist.pushIfNotVisited(contentsOf: destroys.lazy.map { $0 })
  worklist.pushIfNotVisited(contentsOf: nonDestroyingLiverangeExits)
```
created_at: 2025-03-06T08:06:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -124,8 +124,11 @@ private struct Uses {
   // E.g. the none-case of a switch_enum of an Optional.
   private(set) var nonDestroyingLiverangeExits: Stack<Instruction>
 
-  var allLifetimeEndingInstructions: [Instruction] {
-    Array(destroys.lazy.map { $0 }) + Array(nonDestroyingLiverangeExits)
+  // FIXME: Use an InstructionSetVector
path: SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
user: login: eeckstein
body: An `InstructionWorklist` is basically an instruction set-vector. We will probably not add an `InstructionSetVector` in swift. So this comment doesn't make sense.
created_at: 2025-03-06T08:07:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -323,7 +326,12 @@ private func createEndBorrows(for beginBorrow: Value, atEndOf liverange: Instruc
   //   destroy_value %2
   //   destroy_value %3  // The final destroy. Here we need to create the `end_borrow`(s)
   //
-  for endInst in collectedUses.allLifetimeEndingInstructions {
+  var worklist = collectedUses.allLifetimeEndingInstructions
path: SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
user: login: eeckstein
body: Given that the return value of `allLifetimeEndingInstructions` is not a simple Array anymore, but a worklist which has to be `deinitialize`d, it's probably simpler to remove the `var allLifetimeEndingInstructions` and inline the worklist construction directly here.
created_at: 2025-03-06T08:09:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -124,8 +124,11 @@ private struct Uses {
   // E.g. the none-case of a switch_enum of an Optional.
   private(set) var nonDestroyingLiverangeExits: Stack<Instruction>
 
-  var allLifetimeEndingInstructions: [Instruction] {
-    Array(destroys.lazy.map { $0 }) + Array(nonDestroyingLiverangeExits)
+  // FIXME: Use an InstructionSetVector
path: SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
user: login: meg-gupta
body: It worked out here with inlining the worklist construction at the use-site. `InstructionSetVector` may still be valuable in some cases. To use a `InstructionWorklist` in this PR I had to originally copy it to a var and then iterate by popping the elements. 
created_at: 2025-03-06T18:57:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -323,7 +326,12 @@ private func createEndBorrows(for beginBorrow: Value, atEndOf liverange: Instruc
   //   destroy_value %2
   //   destroy_value %3  // The final destroy. Here we need to create the `end_borrow`(s)
   //
-  for endInst in collectedUses.allLifetimeEndingInstructions {
+  var worklist = collectedUses.allLifetimeEndingInstructions
path: SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CopyToBorrowOptimization.swift
user: login: meg-gupta
body: Done. Thanks
created_at: 2025-03-06T18:57:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: meg-gupta
created_at: 2025-03-06T00:41:49Z
body: @swift-ci test
author_association: CONTRIBUTOR
user: login: meg-gupta
created_at: 2025-03-06T18:57:48Z
body: @swift-ci test
author_association: CONTRIBUTOR
user: login: meg-gupta
created_at: 2025-03-06T18:58:08Z
body: @swift-ci test source compatibility
author_association: CONTRIBUTOR
user: login: meg-gupta
created_at: 2025-03-06T19:54:02Z
body: @swift-ci test
author_association: CONTRIBUTOR
user: login: meg-gupta
created_at: 2025-03-06T19:54:46Z
body: @swift-ci test source compatibility
author_association: CONTRIBUTOR
user: login: meg-gupta
created_at: 2025-03-07T08:35:58Z
body: https://github.com/swiftlang/swift-testing/pull/1002

@swift-ci please test Linux
author_association: CONTRIBUTOR