[Concurrency] Asynchronous functions

Composing synchronous functionality on top of asynchronous input. Earlier in the thread, I posted a TStream and ArduousTStream as an example of the diamond problem, where a library would like to encapsulate a control-flow diamond where one branch is asynchronous and the other synchronous. If that carries unacceptable performance overhead, then they might have to thrust the diamond upon their clients.

Here's a simplified version that synchronously parses values off a local buffer, which is asynchronous refilled:

struct TStream {
  // Returns nil when we've exhausted our source
  public mutating func next() async -> T? {
    if mightNeedRefill { await refillInternalBuffer() }
    // ... synchronously vend a T from our internal buffer
  }
}

struct ArduousTStream {
  public var mightNeedRefill: Bool
  public mutating func refillInternalBuffer() async {}

  // Returns nil when we've exhausted our source, if the client remembers
  // to refill the buffer when it needs refilling. Otherwise might return
  // an early nil
  public mutating func next() -> T? {
    // ... synchronously vend a T from our internal buffer
  }
}

If TStream.next() always suspends execution, even though refilling its internal buffer is an infrequent operation, I'm worried that will thrust many libraries into designing something akin to ArduousTStream. But, if the overhead is more on the order of shuffling around some registers, slightly worse data locality for the stack, etc., then this would be far more acceptable to vend TStream.next().

Furthermore, a definite suspension point won't disappear if TStream.next() is marked @inlinable, but (in theory) much of the other overhead could. (If we go the route where suspension points can be sunk into conditionally executed code, then we no longer have the rule that an await always suspends, rather await always suspends until inlining unblocks sinking.)

Michael, I think you're misunderstanding my point, which is not to constrain the implementation, but to simplify the semantic model for users. I'm suggesting that the semantic model is simpler if, at any await, the implementation is free to actually-suspend or not. If you want to write the implementation so that it only shuffles some registers in these particular cases, you would still be free to do so.

My point is simply that it complicates the user's semantic model to think about whether a particular await is actually going to suspend because it's crossing actors, and it doesn't seem to buy much in terms of reasoning about effects because the awaited function itself might await a call that runs on another actor, or be evolved to do so sometime in the future.

Yes, you can reason that calls to the same actor are going to perform better, but that could reasonably be considered an implementation artifact, just like creating a struct instance is going to be more efficient than creating the equivalent class instance. AFAICT it doesn't affect the correctness of the code.