I have a couple of detailed questions below, but I am overall +1 on this proposal.
That said, there is a caveat on this - the proposal isn't clear what happens with unannotated global variables, and the decisions made here have a profound impact on the language, user model, and migration story for Swift Concurrency. There are two ways to handle this: 1) pick a design and try to wrestle this to the ground in this proposal, or 2) land the general design for Global Actors in this proposal, then drive a new "Swift Concurrency meets Global Variables" proposal as a follow-on.
Beyond that major issue, I also have a serious concern about the inference rule for closures (mentioned below). Overall though, this is very nice work. Some train of thought notes below:
The approach of using a user-defined attribute is very nice. I also agree that @MainActor
is a natural name for this thing, aligning with the precedent set by the @main
attribute. The ability to mark both global vars and functions is very nice.
The approach described in Defining Global Actors is very sensible. What is done to prevent creating other instances of the actor? I think we need to prevent this somehow (for memory safety), perhaps with a dynamic check?
In Using global actors on functions and data, it makes total sense to be able to mark methods as pinned onto an actor. How does this interact with API resilience/evolution?
Can protocol requirements be marked as @MyGlobalActor
? (later: yes apparently according to the inference section). It would be great to mention this earlier in the proposal.
Global actor function types: nice. The subtyping rules look great.
In Closures, I'm not sure I understand the last bit:
Are you saying that the @MainActor
aspect is inferred from the type of callback
or are you saying that it is inferred from the type of globalTextSize
which happens to be uttered inside the closure?
The former is super sensible to me, the later is very scary and I think it is unprecedented. The consequence of that is that changing the body of the closure will change the inferred type of the closure, which could lead to downstream changes.
The only thing I'm aware of close to this is inference of the throwing and async bit on the closure. However, that inference is done syntactically and can be done before name lookup. This is a different beast.
I'm +1 on allowing globals to be tagged, this is the core part of the model. However, I'm not sure what is being proposed:
The phrasing "we can require" makes it sound like a decision hasn't been made here, and this is a key part of the design that has to be nailed down.
I think there are some non-controversial cases to cover:
- Explicitly annotated globals are obviously ok and tied to a global.
- Following the precedent of the actor proposal, we should allow
@Sendable let
s to be usable within the module they are defined in. - We could extend #2 to
@frozen
or some other attribute for cross-modulelet
's.
The question is, what do do about unannotated vars, public lets that are used-cross module, etc. I see a few different options with different tradeoffs, all of which provide memory safety:
- Reject them as a compile time error. This will have massive source breakage and isn't a very friendly model.
- Codegen their accessors to produce a runtime error when accessed from a Task (either structured or actor), but allow access from pthreads an other legacy code. This is source compatible and provides progressive migration path, but is a footgun. However, this will be a much easier to detect and fix problem than optional unwraps and array out of bound errors that cause similar traps.
- We could codegen their accessors into implicitly async accessors, sort of treating each global variable as if it is on an anonymous global actor. This would also be a major source break (because you would only be able to touch these in async functions) but would provide nice model over time.
- We can codegen globals to be uninherited unstructured task local values, which don't require
@Sendability
. This would fill in a hole in the design space but more importantly, would provide full source compatibility for Swift concurrency. They do have somewhat new semantics that we'd have to teach, but I think the benefits outweigh the costs.
Coming back to the major point though, I don't know what the proposal is proposing here :-).
What is the proposals model for playgrounds and scripts? We obviously shouldn't have boilerplate annotations like @MainActor
on every top level variable. How does this work / how does the proposal handle this?
At first blush, the inference rules look ok. However, I didn't really think deeply on them and consider all the angles. The meta question I'd think about is: how does this work with resilience and API evolution? Is this all consistent with what we do there?
In Source Compatibility, this claim is made:
Global actors are an additive feature that have no impact on existing source code.
This is true of the core feature, but the handling of global variables is complicated and is almost certain to be source incompatible in some ways. This is the core issue in this proposal.
-Chris