[RBI]: Failure to diagnose invalid use after sends permits data race safety holes

There are some long-standing issues with RBI permitting invalid sends of values across isolation boundaries. Here are some that I've looked at recently:

  1. Missing concurrency diagnostics when a local var is captured by concurent Tasks. · Issue #82827 · swiftlang/swift · GitHub
  2. Non-sendable object is allowed to be passed from actor-isolated region to task region · Issue #86896 · swiftlang/swift · GitHub
  3. Global-actor-isolated-conformance method can be called from outside the actor via a sent object · Issue #89736 · swiftlang/swift · GitHub

The following illustrates the underlying problems:

open class NS {
  func use() {}
}

@MainActor
func send(_ ns: sending NS) {
  Task.detached { ns.use() }
}

// problem: sending semantics aren't enforced
// if the send looks like it's intra-actor

@MainActor
func badSend(_ ns: NS) async {
  send(ns) // ⁉️ passed as sending without error
  ns.use() // ⁉️ and then used after it was sent
}

@MainActor
func badSend2() async {
  // similar sort of thing but with closures
  let ns = NS()
  Task { @MainActor in ns.use() }
  Task.detached { ns.use() } // ⁉️ not diagnosed
}

There are, I think, at least two different issues at play here.

The first seems pretty straightforward – sending arguments are ignored in cases where an element's isolation region and a callee it's passed to have "matching" actor isolation. Due to how this is implemented, it means subsequent use of a value that was passed to a sending parameter within that isolation region is permitted even though the callee that received the value is allowed to use it concurrently with respect to the caller.

This seems to occur due to this logic in PartitionUtils.h when processing a send:

// If our callee and region are both actor isolated and part of the same
// isolation domain, do not treat this as a send.
if (calleeIsolationInfo.isActorIsolated() &&
    sentRegionIsolation.hasSameIsolation(calleeIsolationInfo))
  return;

But slightly further down in that method, what looks like a related condition for handling disconnected regions has a similar check, but explicitly avoids the send elision if the argument being sent is marked sending:

// Next see if we are disconnected and have the same isolation. In such a
// case, if we are not marked explicitly as sending, we do not send
// since the disconnected value is allowed to be resued after we
// return. If we are passed as a sending parameter, we cannot do this.
if (auto *sourceInst = Impl::getSourceInst(op)) {
  if (auto fas = FullApplySite::isa(sourceInst);
      (!fas || !fas.isSending(*op.getSourceOp())) && // 👈
      sentRegionIsolation.isDisconnected() && calleeIsolationInfo &&
      sentRegionIsolation.hasSameIsolation(calleeIsolationInfo))
    return;
}

It seems easy enough to fix that I think, by just checking the same state in both cases, and I've attempted to do so here.

How to address the similar-looking problem with closures however seems less clear to me. In cases like this:

@MainActor
func passIsolatedClosure(
    _ c: @escaping @MainActor () async -> Void
) {}

@concurrent
func useConcurrently(_: NS) async {}

@MainActor
func bug() async {
    let ns = NS()
    passIsolatedClosure { ns.use() }
    await useConcurrently(ns)
}

Region analysis actually would produce an error at the point that ns is passed to useConcurrently(), but such errors are never surfaced because there is logic in the partition op evaluator that suppresses it:

// If our instruction does not have any isolation info associated with it,
// it must be nonisolated. See if our function has a matching isolation to
// our sent operand. If so, we can squelch this.
auto functionIsolation =
    sentOp->getUser()->getFunction()->getActorIsolation();
if (functionIsolation.isActorIsolated() &&
    SILIsolationInfo::get(sentOp->getUser())
        .hasSameIsolation(functionIsolation))
return;
}

In this case, the region analysis log is something like this:

Full function log...
╾++++++++++++++++++++++++++++++╼
Beginning processing: $s6output3bugyyYaF
Demangled: bug()
╾++++++++++++++++++++++++++++++╼
Dump:
// bug()
// Isolation: global_actor. type: MainActor
sil hidden [ossa] @$s6output3bugyyYaF : $@convention(thin) @async () -> () {
bb0:
  %0 = metatype $@thick MainActor.Type            // user: %2
  // function_ref static MainActor.shared.getter
  %1 = function_ref @$sScM6sharedScMvgZ : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // user: %2
  %2 = apply %1(%0) : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // users: %3, %17, %19
  hop_to_executor %2                              // id: %3
  %4 = metatype $@thick NS.Type                   // user: %6
  // function_ref NS.__allocating_init()
  %5 = function_ref @$s6output2NSCACycfC : $@convention(method) (@thick NS.Type) -> @owned NS // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
  %7 = move_value [lexical] [var_decl] %6         // users: %16, %18, %10, %8
  debug_value %7, let, name "ns"                  // id: %8
  // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
  %10 = copy_value %7                             // user: %11
  %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
  // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
  %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
  destroy_value %11                               // id: %14
  // function_ref useConcurrently(_:)
  %15 = function_ref @$s6output15useConcurrentlyyyAA2NSCYaF : $@convention(thin) @async (@guaranteed NS) -> () // user: %16
  %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
  hop_to_executor %2                              // id: %17
  destroy_value %7                                // id: %18
  destroy_value %2                                // id: %19
  %20 = tuple ()                                  // user: %21
  return %20                                      // id: %21
} // end sil function '$s6output3bugyyYaF'

╾──────────────────────────────╼
Performing pre-dataflow scan to gather flow insensitive information $s6output3bugyyYaF:
╾──────────────────────────────╼
Initializing Function Args:
    None.
╾──────────────────────────────╼
Compiling basic block for function $s6output3bugyyYaF: bb0
╾──────────────────────────────╼
sil_scope 1 { loc "<source>":139:6 parent @$s6output3bugyyYaF : $@convention(thin) @async () -> () }
sil_scope 2 { loc "<source>":140:14 parent 1 }
sil_scope 3 { loc "<source>":140:9 parent 1 }
bb0:
  %0 = metatype $@thick MainActor.Type            // user: %2
  // function_ref static MainActor.shared.getter
  %1 = function_ref @$sScM6sharedScMvgZ : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // user: %2
  %2 = apply %1(%0) : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // users: %3, %17, %19
  hop_to_executor %2                              // id: %3
  %4 = metatype $@thick NS.Type                   // user: %6
  // function_ref NS.__allocating_init()
  %5 = function_ref @$s6output2NSCACycfC : $@convention(method) (@thick NS.Type) -> @owned NS // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
  %7 = move_value [lexical] [var_decl] %6         // users: %16, %18, %10, %8
  debug_value %7, let, name "ns"                  // id: %8
  // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
  %10 = copy_value %7                             // user: %11
  %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
  // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
  %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
  destroy_value %11                               // id: %14
  // function_ref useConcurrently(_:)
  %15 = function_ref @$s6output15useConcurrentlyyyAA2NSCYaF : $@convention(thin) @async (@guaranteed NS) -> () // user: %16
  %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
  hop_to_executor %2                              // id: %17
  destroy_value %7                                // id: %18
  destroy_value %2                                // id: %19
  %20 = tuple ()                                  // user: %21
  return %20                                      // id: %21
╾──────────────────────────────╼
Results:
Visiting:   %0 = metatype $@thick MainActor.Type            // user: %2
    Semantics: assign_fresh
Visiting:   // function_ref static MainActor.shared.getter
  %1 = function_ref @$sScM6sharedScMvgZ : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // user: %2
    Semantics: assign_fresh
Visiting:   %2 = apply %1(%0) : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // users: %3, %17, %19
    Semantics: apply
Visiting:   hop_to_executor %2 : $MainActor                 // id: %3
    Semantics: ignored
Visiting:   %4 = metatype $@thick NS.Type                   // user: %6
    Semantics: assign_fresh
Visiting:   // function_ref NS.__allocating_init()
  %5 = function_ref @$s6output2NSCACycfC : $@convention(method) (@thick NS.Type) -> @owned NS // user: %6
    Semantics: assign_fresh
Visiting:   %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
    Semantics: apply
 ┌─┬─╼  %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
 │ └─╼  line:140:14
 ├─────╼ assign_fresh %%5:   %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
 └─────╼ Used Values
          └╼ State: %%5. TrackableValueState[id: 5][is_no_alias: no][is_sendable: no][region_value_kind: disconnected].
             Rep Value:   %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
             Type: $NS
Visiting:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
    Semantics: assign
 ┌─┬─╼  %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
 │ └─╼  line:140:14
 ├─────╼ require %%5:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
 │    └╼ assign_direct %%6 = %%5:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
 └─────╼ Used Values
          └╼ State: %%5. TrackableValueState[id: 5][is_no_alias: no][is_sendable: no][region_value_kind: disconnected].
             Rep Value:   %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
             Type: $NS
          └╼ State: %%6. TrackableValueState[id: 6][is_no_alias: no][is_sendable: no][region_value_kind: disconnected].
             Rep Value:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
             Type: $NS
Visiting:   debug_value %7 : $NS, let, name "ns"            // id: %8
    Semantics: ignored
Visiting:   // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
    Semantics: assign_fresh
 ┌─┬─╼  // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
 │ └─╼  line:141:25
 ├─────╼ assign_fresh %%7:   // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
 └─────╼ Used Values
          └╼ State: %%7. TrackableValueState[id: 7][is_no_alias: no][is_sendable: no][region_value_kind: main actor-isolated].
             Rep Value:   // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
             Type: $@convention(thin) @Sendable @async (@guaranteed NS) -> ()
Visiting:   %10 = copy_value %7 : $NS                       // user: %11
    Semantics: look_through
Visiting:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
Translating Isolated Partial Apply!
    Semantics: special
 ┌─┬─╼  %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
 │ └─╼  line:141:25
 ├─────╼ require %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
 │    └╼ send %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
 └─────╼ Used Values
          └╼ State: %%6. TrackableValueState[id: 6][is_no_alias: no][is_sendable: no][region_value_kind: disconnected].
             Rep Value:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
             Type: $NS
Visiting:   // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
    Semantics: assign_fresh
 ┌─┬─╼  // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
 │ └─╼  line:141:5
 ├─────╼ assign_fresh %%8:   // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
 └─────╼ Used Values
          └╼ State: %%8. TrackableValueState[id: 8][is_no_alias: no][is_sendable: no][region_value_kind: main actor-isolated].
             Rep Value:   // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
             Type: $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
Visiting:   %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
    Semantics: apply
 ┌─┬─╼  %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
 │ └─╼  line:141:5
 ├─────╼ require %%8:   %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
 └─────╼ Used Values
          └╼ State: %%8. TrackableValueState[id: 8][is_no_alias: no][is_sendable: no][region_value_kind: main actor-isolated].
             Rep Value:   // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
             Type: $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
Visiting:   destroy_value %11 : $@Sendable @async @callee_guaranteed () -> () // id: %14
    Semantics: ignored
Visiting:   // function_ref useConcurrently(_:)
  %15 = function_ref @$s6output15useConcurrentlyyyAA2NSCYaF : $@convention(thin) @async (@guaranteed NS) -> () // user: %16
    Semantics: assign_fresh
Visiting:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
    Semantics: apply
 ┌─┬─╼  %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
 │ └─╼  line:142:11
 ├─────╼ require %%6:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
 │    └╼ send %%6:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
 └─────╼ Used Values
          └╼ State: %%6. TrackableValueState[id: 6][is_no_alias: no][is_sendable: no][region_value_kind: disconnected].
             Rep Value:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
             Type: $NS
Visiting:   hop_to_executor %2 : $MainActor                 // id: %17
    Semantics: ignored
Visiting:   destroy_value %7 : $NS                          // id: %18
    Semantics: ignored
Visiting:   destroy_value %2 : $MainActor                   // id: %19
    Semantics: ignored
Visiting:   %20 = tuple ()                                  // user: %21
    Semantics: assign
Visiting:   return %20 : $()                                // id: %21
    Semantics: require
╾──────────────────────────────╼
Performing Dataflow!
╾──────────────────────────────╼
Values!
%%0: TrackableValue. State: TrackableValueState[id: 0][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   %0 = metatype $@thick MainActor.Type            // user: %2
%%1: TrackableValue. State: TrackableValueState[id: 1][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   // function_ref static MainActor.shared.getter
  %1 = function_ref @$sScM6sharedScMvgZ : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // user: %2
%%2: TrackableValue. State: TrackableValueState[id: 2][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   %2 = apply %1(%0) : $@convention(method) (@thick MainActor.Type) -> @owned MainActor // users: %3, %17, %19
%%3: TrackableValue. State: TrackableValueState[id: 3][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   %4 = metatype $@thick NS.Type                   // user: %6
%%4: TrackableValue. State: TrackableValueState[id: 4][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   // function_ref NS.__allocating_init()
  %5 = function_ref @$s6output2NSCACycfC : $@convention(method) (@thick NS.Type) -> @owned NS // user: %6
%%5: TrackableValue. State: TrackableValueState[id: 5][is_no_alias: no][is_sendable: no][region_value_kind: disconnected].
    Rep Value:   %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
%%6: TrackableValue. State: TrackableValueState[id: 6][is_no_alias: no][is_sendable: no][region_value_kind: disconnected].
    Rep Value:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
%%7: TrackableValue. State: TrackableValueState[id: 7][is_no_alias: no][is_sendable: no][region_value_kind: main actor-isolated].
    Rep Value:   // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
%%8: TrackableValue. State: TrackableValueState[id: 8][is_no_alias: no][is_sendable: no][region_value_kind: main actor-isolated].
    Rep Value:   // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
%%9: TrackableValue. State: TrackableValueState[id: 9][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
%%10: TrackableValue. State: TrackableValueState[id: 10][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
%%11: TrackableValue. State: TrackableValueState[id: 11][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   // function_ref useConcurrently(_:)
  %15 = function_ref @$s6output15useConcurrentlyyyAA2NSCYaF : $@convention(thin) @async (@guaranteed NS) -> () // user: %16
%%12: TrackableValue. State: TrackableValueState[id: 12][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
%%13: TrackableValue. State: TrackableValueState[id: 13][is_no_alias: no][is_sendable: yes][region_value_kind: disconnected].
    Rep Value:   %20 = tuple ()                                  // user: %21
Block: bb0
    Visiting Preds!
Applying: assign_fresh %%5:   %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
    Before: []
    After:  [(5)]
Applying: require %%5:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
    Before: [(5)]
    After:  [(5)]
Applying: assign_direct %%6 = %%5:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
    Before: [(5)]
    After:  [(5 6)]
Applying: assign_fresh %%7:   // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
    Before: [(5 6)]
    After:  [(5 6)(main actor-isolated: 7)]
Applying: require %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
    Before: [(5 6)(main actor-isolated: 7)]
    After:  [(5 6)(main actor-isolated: 7)]
Applying: send %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
    Before: [(5 6)(main actor-isolated: 7)]
    After:  [{5 6}(main actor-isolated: 7)]
Applying: assign_fresh %%8:   // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
    Before: [{5 6}(main actor-isolated: 7)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
Applying: require %%8:   %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
    Before: [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
Applying: require %%6:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
    Before: [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
Applying: send %%6:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
    Before: [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    Working Partition: [{5 6}(7)(8)]
    Exit Partition: [{5 6}(7)(8)]
    Updated Partition: yes
===> PROCESSING: $s6output3bugyyYaF
Emitting diagnostics for function $s6output3bugyyYaF
Walking blocks for diagnostics.
|--> Block bb0
Entry Partition: []
Applying: assign_fresh %%5:   %6 = apply %5(%4) : $@convention(method) (@thick NS.Type) -> @owned NS // user: %7
    Before: []
    After:  [(5)]
Applying: require %%5:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
    Before: [(5)]
    After:  [(5)]
Applying: assign_direct %%6 = %%5:   %7 = move_value [lexical] [var_decl] %6 : $NS   // users: %16, %18, %10, %8
    Before: [(5)]
    After:  [(5 6)]
Applying: assign_fresh %%7:   // function_ref closure #1 in bug()
  %9 = function_ref @$s6output3bugyyYaFyyYaYbScMYccfU_ : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // user: %11
    Before: [(5 6)]
    After:  [(5 6)(main actor-isolated: 7)]
Applying: require %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
    Before: [(5 6)(main actor-isolated: 7)]
    After:  [(5 6)(main actor-isolated: 7)]
Applying: send %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
    Before: [(5 6)(main actor-isolated: 7)]
    After:  [{5 6}(main actor-isolated: 7)]
Applying: assign_fresh %%8:   // function_ref passIsolatedClosure(_:)
  %12 = function_ref @$s6output19passIsolatedClosureyyyyYaYbScMYccF : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> () // user: %13
    Before: [{5 6}(main actor-isolated: 7)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
Applying: require %%8:   %13 = apply %12(%11) : $@convention(thin) (@guaranteed @Sendable @async @callee_guaranteed () -> ()) -> ()
    Before: [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
Applying: require %%6:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
    Before: [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
Applying: send %%6:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
    Before: [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
Exit Partition: [{5 6}(7)(8)]
Finished walking blocks for diagnostics.

Two things in the dataflow logs seem confusing to me. There's this bit where ns is captured by the isolated partial apply (closure), and sent:

Applying: require %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
    Before: [(5 6)(main actor-isolated: 7)]
    After:  [(5 6)(main actor-isolated: 7)]
Applying: send %%6:   %11 = partial_apply [callee_guaranteed] %9(%10) : $@convention(thin) @Sendable @async (@guaranteed NS) -> () // users: %14, %13
    Before: [(5 6)(main actor-isolated: 7)]
    After:  [{5 6}(main actor-isolated: 7)]

The region containing ns is marked sent, which seems correct, but it is seemingly not merged with the main actor. Then at the subsequent apply of the @concurrent function, we have:

Applying: require %%6:   %16 = apply %15(%7) : $@convention(thin) @async (@guaranteed NS) -> ()
    Before: [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]
    After:  [{5 6}(main actor-isolated: 7)(main actor-isolated: 8)]

The require here seems like it should produce a use after send error, since ns's region was already sent. And if you add extra debug logging, you can see that it essentially does, but as I mentioned above, that error is never surfaced because of the suppression logic.

Something I'm confused about regarding the condition in the suppression code is that it doesn't appear to consult the partition op's instruction for anything... should it? I.e. the current logic reasons entirely from sentOp->getUser() and doesn't use op at all.

I've tried a couple small localized changes to try and fix this issue by adjusting that error suppression logic, but none have done so without regressing something else. I'm wondering if this particular error suppression mechanism is actually desirable or if there is an alternative way to model things that would prevent that code from being necessary.

FYI @Michael_Gottesman.

4 Likes