Files
swift-mirror/test/SILOptimizer/moveonly_addresschecker_convert_function_onstack.sil
Ben Cohen f0a8437191 MoveOnlyChecker: look through convert_function when closing an on-stack partial_apply's borrow scope (#88805)
- **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.
2026-05-05 05:48:29 -07:00

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 : $()
}