Skip to content

Commit 108a02f

Browse files
committed
[semantic-arc-opts] Fix small bug around eliminating copies from begin_borrow, load_borrow, and dead end blocks.
TLDR: The bug occurs since a copy_value does not need to be balanced by destroy_values along paths that end in dead end blocks. So our check that all "consuming" uses of the copy_value are within the lifetime of the begin_borrow, load_borrow trivially succeed since there are no consuming uses of the copy_value to check! This then results in us creating a use of a borrowed value after the borrowed values end borrow. I go through the bug in detail (via an example) and provide a proof that this bug can only occur if the copy_value if there exists a set of dead end blocks that jointly post-dominate the copy. ---- Consider the following SIL: ``` %1 = begin_borrow %0 : $KlassPair (1) %2 = struct_extract %1 : $KlassPair, #KlassPair.firstKlass %3 = copy_value %2 : $Klass ... end_borrow %1 : $LintCommand (2) cond_br ..., bb1, bb2 ... bbN: // Never return type implies dead end block. apply %f(%3) : $@convention(thin) (@guaranteed Klass) -> Never (3) unreachable ``` For simplicity, note that if bbN post-dominates %3, given that when we compute linear lifetime errors we ignore dead end blocks, we would not register that the copy_values only use is outside of the begin_borrow region defined by (1), (2) and thus would eliminate the copy. This would result in %2 being used by %f, causing the linear lifetime checker to error. Naively one may assume that the solution to this is to just check if %3 has /any/ destroy_values at all and if it doesn't have any reachable destroy_values, then we are in this case. But is this correct in general? We prove this below: The only paths along which the copy_value can not be destroyed or consumed is along paths to dead end blocks. Trivially, we know that such a dead end block, can not be reachable from the end_borrow since by their definition dead end blocks do not have any successor instructions or blocks. So we know that we can only run into this bug if we have a dead end block reachable from the end_borrow, meaning that the bug can not occur if we branch before the end_borrow since in that case, the borrow scope would last over the dead end block's no return meaning that we will not use the borrowed value after its lifetime is ended by the end_borrow. With that in hand, we note again that if we have exactly one consumed, destroy_value /after/ the end_borrow we will not optimize here since the optimization only attempts to eliminate copies that are not consumed by non-destroy_value instructions. This means that this bug can only occur if the copy_value is only post-dominated by dead end blocks that use the value in a non-consuming way. NOTE: This can only occur if the copy_value is from a "borrow introducer" that is associated with an end_borrow. This today includes begin_borrow, load_borrow, but importantly and notably not function arguments. rdar://54788632
1 parent faaa3a8 commit 108a02f

File tree

2 files changed

+119
-1
lines changed

2 files changed

+119
-1
lines changed

lib/SILOptimizer/Mandatory/SemanticARCOpts.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,62 @@ bool SemanticARCOptVisitor::performGuaranteedCopyValueOptimization(CopyValueInst
340340
if (isConsumed(cvi, destroys, &guaranteedForwardingInsts))
341341
return false;
342342

343+
// Next check if we have any destroys at all of our copy_value and an operand
344+
// that is not a function argument. Otherwise, due to the way we ignore dead
345+
// end blocks, we may eliminate the copy_value, creating a use of the borrowed
346+
// value after the end_borrow.
347+
//
348+
// DISCUSSION: Consider the following SIL:
349+
//
350+
// ```
351+
// %1 = begin_borrow %0 : $KlassPair (1)
352+
// %2 = struct_extract %1 : $KlassPair, #KlassPair.firstKlass
353+
// %3 = copy_value %2 : $Klass
354+
// ...
355+
// end_borrow %1 : $LintCommand (2)
356+
// cond_br ..., bb1, bb2
357+
//
358+
// ...
359+
//
360+
// bbN:
361+
// // Never return type implies dead end block.
362+
// apply %f(%3) : $@convention(thin) (@guaranteed Klass) -> Never (3)
363+
// unreachable
364+
// ```
365+
//
366+
// For simplicity, note that if bbN post-dominates %3, given that when we
367+
// compute linear lifetime errors we ignore dead end blocks, we would not
368+
// register that the copy_values only use is outside of the begin_borrow
369+
// region defined by (1), (2) and thus would eliminate the copy. This would
370+
// result in %2 being used by %f, causing the linear lifetime checker to
371+
// error.
372+
//
373+
// Naively one may assume that the solution to this is to just check if %3 has
374+
// /any/ destroy_values at all and if it doesn't have any reachable
375+
// destroy_values, then we are in this case. But is this correct in
376+
// general. We prove this below:
377+
//
378+
// The only paths along which the copy_value can not be destroyed or consumed
379+
// is along paths to dead end blocks. Trivially, we know that such a dead end
380+
// block, can not be reachable from the end_borrow since by their nature dead
381+
// end blocks end in unreachables.
382+
//
383+
// So we know that we can only run into this bug if we have a dead end block
384+
// reachable from the end_borrow, meaning that the bug can not occur if we
385+
// branch before the end_borrow since in that case, the borrow scope would
386+
// last over the dead end block's no return meaning that we will not use the
387+
// borrowed value after its lifetime is ended by the end_borrow.
388+
//
389+
// With that in hand, we note again that if we have exactly one consumed,
390+
// destroy_value /after/ the end_borrow we will not optimize here. This means
391+
// that this bug can only occur if the copy_value is only post-dominated by
392+
// dead end blocks that use the value in a non-consuming way.
393+
if (destroys.empty() && llvm::any_of(borrowIntroducers, [](SILValue v) {
394+
return !isa<SILFunctionArgument>(v);
395+
})) {
396+
return false;
397+
}
398+
343399
// If we reached this point, then we know that all of our users can
344400
// accept a guaranteed value and our owned value is destroyed only
345401
// by destroy_value. Check if all of our destroys are joint

test/SILOptimizer/semantic-arc-opts.sil

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import Builtin
88
// Declarations //
99
//////////////////
1010

11+
enum MyNever {}
12+
1113
sil @guaranteed_user : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
1214
sil @owned_user : $@convention(thin) (@owned Builtin.NativeObject) -> ()
1315
sil @get_owned_obj : $@convention(thin) () -> @owned Builtin.NativeObject
16+
sil @unreachable_guaranteed_user : $@convention(thin) (@guaranteed Builtin.NativeObject) -> MyNever
1417

1518
struct NativeObjectPair {
1619
var obj1 : Builtin.NativeObject
@@ -775,4 +778,63 @@ bb0(%0 : $*NativeObjectPair):
775778
destroy_addr %0 : $*NativeObjectPair
776779
%9999 = tuple()
777780
return %9999 : $()
778-
}
781+
}
782+
783+
// Make sure we do not eliminate the copy_value below to ensure that all uses of
784+
// %2 are before %2's end_borrow.
785+
//
786+
// We used to eliminate the copy_value and change %func to use %2.
787+
//
788+
// CHECK-LABEL: sil [ossa] @begin_borrow_used_by_postdominating_no_return_function : $@convention(thin) () -> MyNever {
789+
// CHECK: copy_value
790+
// CHECK: } // end sil function 'begin_borrow_used_by_postdominating_no_return_function'
791+
sil [ossa] @begin_borrow_used_by_postdominating_no_return_function : $@convention(thin) () -> MyNever {
792+
bb0:
793+
%0 = function_ref @get_nativeobject_pair : $@convention(thin) () -> @owned NativeObjectPair
794+
%1 = apply %0() : $@convention(thin) () -> @owned NativeObjectPair
795+
%2 = begin_borrow %1 : $NativeObjectPair
796+
%3 = struct_extract %2 : $NativeObjectPair, #NativeObjectPair.obj1
797+
%4 = copy_value %3 : $Builtin.NativeObject
798+
end_borrow %2 : $NativeObjectPair
799+
%func = function_ref @unreachable_guaranteed_user : $@convention(thin) (@guaranteed Builtin.NativeObject) -> MyNever
800+
apply %func(%4) : $@convention(thin) (@guaranteed Builtin.NativeObject) -> MyNever
801+
unreachable
802+
}
803+
804+
// Make sure we do not eliminate the copy_value below to ensure that all uses of
805+
// %2 are before %2's end_borrow.
806+
//
807+
// We used to eliminate the copy_value and change %func to use %2.
808+
//
809+
// CHECK-LABEL: sil [ossa] @load_borrow_used_by_postdominating_no_return_function : $@convention(thin) () -> MyNever {
810+
// CHECK: copy_value
811+
// CHECK: } // end sil function 'load_borrow_used_by_postdominating_no_return_function'
812+
sil [ossa] @load_borrow_used_by_postdominating_no_return_function : $@convention(thin) () -> MyNever {
813+
bb0:
814+
%0 = function_ref @get_nativeobject_pair : $@convention(thin) () -> @owned NativeObjectPair
815+
%1 = apply %0() : $@convention(thin) () -> @owned NativeObjectPair
816+
%stackSlot = alloc_stack $NativeObjectPair
817+
store %1 to [init] %stackSlot : $*NativeObjectPair
818+
%2 = load_borrow %stackSlot : $*NativeObjectPair
819+
%3 = struct_extract %2 : $NativeObjectPair, #NativeObjectPair.obj1
820+
%4 = copy_value %3 : $Builtin.NativeObject
821+
end_borrow %2 : $NativeObjectPair
822+
%func = function_ref @unreachable_guaranteed_user : $@convention(thin) (@guaranteed Builtin.NativeObject) -> MyNever
823+
apply %func(%4) : $@convention(thin) (@guaranteed Builtin.NativeObject) -> MyNever
824+
unreachable
825+
}
826+
827+
// Make sure we do perform the optimization if our borrowed value is an
828+
// argument.
829+
//
830+
// CHECK-LABEL: sil [ossa] @guaranteed_arg_used_by_postdominating_no_return_function : $@convention(thin) (@guaranteed NativeObjectPair) -> MyNever {
831+
// CHECK-NOT: copy_value
832+
// CHECK: } // end sil function 'guaranteed_arg_used_by_postdominating_no_return_function'
833+
sil [ossa] @guaranteed_arg_used_by_postdominating_no_return_function : $@convention(thin) (@guaranteed NativeObjectPair) -> MyNever {
834+
bb0(%0 : @guaranteed $NativeObjectPair):
835+
%3 = struct_extract %0 : $NativeObjectPair, #NativeObjectPair.obj1
836+
%4 = copy_value %3 : $Builtin.NativeObject
837+
%func = function_ref @unreachable_guaranteed_user : $@convention(thin) (@guaranteed Builtin.NativeObject) -> MyNever
838+
apply %func(%4) : $@convention(thin) (@guaranteed Builtin.NativeObject) -> MyNever
839+
unreachable
840+
}

0 commit comments

Comments
 (0)