mirror of
https://github.com/apple/swift.git
synced 2026-06-20 15:42:51 +02:00
f0a8437191
- **Explanation**: Defensive prep for an upcoming SILGen change. Teaches
`CopiedLoadBorrowEliminationVisitor` to recognize a noncopyable
`load_borrow` → on-stack `partial_apply` → `convert_function` →
`destroy_value` chain, instead of hitting `llvm_unreachable("We should
never hit this")`
- **Scope**: Narrow change to one case of one visitor in the move-only
checker. The crash isn't reachable from vanilla Swift source on current
`main` — no code path produces the triggering SIL shape today. Lands
ahead of a SILGen change that will route noncopyable `let` captures into
non-escaping closures through on-stack `partial_apply`, at which point a
`@MainActor`-isolated noescape async closure body introduces the
intervening `convert_function` (stripping `@Sendable`) that this fix
accommodates.
- **Risk**: Very low. The edit is scoped to the
`ForwardingConsume`/`DestroyingConsume` arm of a single `switch` in
`MoveOnlyAddressCheckerUtils.cpp::CopiedLoadBorrowEliminationVisitor::visitUse`.
The original invariant (only `destroy_value` users accepted) is
preserved by an explicit user-type check, but broadened to include
`convert_function` chains back to the on-stack `partial_apply`. Both
`llvm_unreachable`s still fire for anything outside the expected shape.
The broadened path only fires on a SIL shape that current SILGen doesn't
produce, so in principle no existing test should exercise it — this is a
defensive-preparation change whose behavioral effect is required for an
upcoming SILGen PR.
- **Testing**: New `.sil` regression test at
`test/SILOptimizer/moveonly_addresschecker_convert_function_onstack.sil`
with two variants (single `convert_function` between PA and destroy;
chained pair). Verified by reverting the fix — the
single-`convert_function` variant triggers exactly the
`llvm_unreachable` it's designed to catch.
`CopiedLoadBorrowEliminationVisitor` walks forward from a noncopyable
`load_borrow`, tracking uses via `OperandOwnership`. An on-stack
`partial_apply` sees its `load_borrow` operand as
`OperandOwnership::Borrow`; the `Borrow` case then walks the PA's
results. Those PA-result uses can have
`OperandOwnership::ForwardingConsume`/`DestroyingConsume` (on-stack PA
has `OwnershipKind::Owned` in OSSA, and `convert_function` forwards that
ownership). The existing special case only recognized a direct
`destroy_value` of the PA and fired `llvm_unreachable` on any
intermediary.
The fix keeps the original syntactic invariant — the user must be a
`destroy_value` or a `convert_function` — and broadens the operand check
to walk backward through a `convert_function` chain to find the
underlying `PartialApplyInst`. If it's on-stack, the consuming use
closes the borrow scope rather than consuming the captured noncopyable.
When the use is itself a `convert_function` (forwarding consume rather
than terminating), its uses are pushed onto the worklist so the
downstream `destroy_value`/`apply` is visited with the right context.
Mirrors the existing look-through-`convert_function` treatment in the
forwarding traversal earlier in this file. Deliberately does not
generalize to other forwarding owners (e.g., `move_value`,
`mark_dependence`, `convert_escape_to_noescape`) since they are not
produced by SILGen between an on-stack PA and its destroy today.
86 lines
4.8 KiB
Plaintext
86 lines
4.8 KiB
Plaintext
// RUN: %target-sil-opt -sil-print-types -module-name moveonly -sil-move-only-address-checker -enable-sil-verify-all %s | %FileCheck %s
|
|
|
|
// Regression test for a crash in
|
|
// `CopiedLoadBorrowEliminationVisitor::visitUse` (in
|
|
// `MoveOnlyAddressCheckerUtils.cpp`). A noncopyable `load_borrow`
|
|
// captured by an on-stack `partial_apply` whose result flows through a
|
|
// `convert_function` (to strip `@Sendable` from a
|
|
// `@MainActor`-isolated noescape async closure) hit
|
|
// `llvm_unreachable("We should never hit this")` because the
|
|
// `ForwardingConsume`/`DestroyingConsume` special-case only recognized
|
|
// a direct `destroy_value` of the partial_apply, not one reached via
|
|
// `convert_function`.
|
|
//
|
|
// The checker should now look through `convert_function` and recognize
|
|
// that the lifetime-ending use still closes the on-stack PA's borrow
|
|
// scope, not consumes the captured noncopyable.
|
|
|
|
sil_stage raw
|
|
|
|
import Swift
|
|
import Builtin
|
|
|
|
public class NonSendableKlass {}
|
|
|
|
public struct NoncopyableStructNonsendable : ~Copyable {
|
|
var k: NonSendableKlass
|
|
}
|
|
|
|
sil @asyncMainActorClosureBody : $@convention(thin) @Sendable @async (@guaranteed NoncopyableStructNonsendable) -> ()
|
|
sil @nonescapingAsyncConsumer : $@convention(thin) (@guaranteed @noescape @async @callee_guaranteed () -> ()) -> ()
|
|
|
|
// The checker previously crashed while processing this function. With the
|
|
// fix, the mark is recognized as a valid borrowed init and stripped; the
|
|
// partial_apply + convert_function + destroy chain is preserved.
|
|
//
|
|
// CHECK-LABEL: sil [ossa] @convertFunctionDestroyOfOnStackPA : $@convention(thin) (@guaranteed { let NoncopyableStructNonsendable }) -> () {
|
|
// CHECK: [[PROJ:%.*]] = project_box
|
|
// CHECK: [[LB:%.*]] = load_borrow [[PROJ]]
|
|
// CHECK: [[PAI:%.*]] = partial_apply [callee_guaranteed] [on_stack] {{%.*}}([[LB]])
|
|
// CHECK: [[CVT:%.*]] = convert_function [[PAI]]
|
|
// CHECK: apply {{%.*}}([[CVT]])
|
|
// CHECK: destroy_value [[CVT]]
|
|
// CHECK: end_borrow [[LB]]
|
|
// CHECK: } // end sil function 'convertFunctionDestroyOfOnStackPA'
|
|
sil [ossa] @convertFunctionDestroyOfOnStackPA : $@convention(thin) (@guaranteed { let NoncopyableStructNonsendable }) -> () {
|
|
bb0(%0 : @closureCapture @guaranteed ${ let NoncopyableStructNonsendable }):
|
|
%1 = project_box %0, 0
|
|
%2 = function_ref @asyncMainActorClosureBody : $@convention(thin) @Sendable @async (@guaranteed NoncopyableStructNonsendable) -> ()
|
|
%3 = mark_unresolved_non_copyable_value [no_consume_or_assign] %1 : $*NoncopyableStructNonsendable
|
|
%4 = load_borrow %3 : $*NoncopyableStructNonsendable
|
|
%5 = partial_apply [callee_guaranteed] [on_stack] %2(%4) : $@convention(thin) @Sendable @async (@guaranteed NoncopyableStructNonsendable) -> ()
|
|
%6 = convert_function %5 to $@noescape @async @callee_guaranteed () -> ()
|
|
%7 = function_ref @nonescapingAsyncConsumer : $@convention(thin) (@guaranteed @noescape @async @callee_guaranteed () -> ()) -> ()
|
|
%8 = apply %7(%6) : $@convention(thin) (@guaranteed @noescape @async @callee_guaranteed () -> ()) -> ()
|
|
destroy_value %6 : $@noescape @async @callee_guaranteed () -> ()
|
|
end_borrow %4 : $NoncopyableStructNonsendable
|
|
%9 = tuple ()
|
|
return %9 : $()
|
|
}
|
|
|
|
// Chained convert_functions between the on-stack partial_apply and its
|
|
// destroy also need to be walked through.
|
|
//
|
|
// CHECK-LABEL: sil [ossa] @chainedConvertFunctionDestroyOfOnStackPA : $@convention(thin) (@guaranteed { let NoncopyableStructNonsendable }) -> () {
|
|
// CHECK: [[PAI:%.*]] = partial_apply [callee_guaranteed] [on_stack]
|
|
// CHECK: [[CVT1:%.*]] = convert_function [[PAI]]
|
|
// CHECK: [[CVT2:%.*]] = convert_function [[CVT1]]
|
|
// CHECK: destroy_value [[CVT2]]
|
|
// CHECK: } // end sil function 'chainedConvertFunctionDestroyOfOnStackPA'
|
|
sil [ossa] @chainedConvertFunctionDestroyOfOnStackPA : $@convention(thin) (@guaranteed { let NoncopyableStructNonsendable }) -> () {
|
|
bb0(%0 : @closureCapture @guaranteed ${ let NoncopyableStructNonsendable }):
|
|
%1 = project_box %0, 0
|
|
%2 = function_ref @asyncMainActorClosureBody : $@convention(thin) @Sendable @async (@guaranteed NoncopyableStructNonsendable) -> ()
|
|
%3 = mark_unresolved_non_copyable_value [no_consume_or_assign] %1 : $*NoncopyableStructNonsendable
|
|
%4 = load_borrow %3 : $*NoncopyableStructNonsendable
|
|
%5 = partial_apply [callee_guaranteed] [on_stack] %2(%4) : $@convention(thin) @Sendable @async (@guaranteed NoncopyableStructNonsendable) -> ()
|
|
%6 = convert_function %5 to $@noescape @Sendable @async @callee_guaranteed () -> ()
|
|
%7 = convert_function %6 to $@noescape @async @callee_guaranteed () -> ()
|
|
%8 = function_ref @nonescapingAsyncConsumer : $@convention(thin) (@guaranteed @noescape @async @callee_guaranteed () -> ()) -> ()
|
|
%9 = apply %8(%7) : $@convention(thin) (@guaranteed @noescape @async @callee_guaranteed () -> ()) -> ()
|
|
destroy_value %7 : $@noescape @async @callee_guaranteed () -> ()
|
|
end_borrow %4 : $NoncopyableStructNonsendable
|
|
%10 = tuple ()
|
|
return %10 : $()
|
|
}
|