[Pitch] A typealias for per-file default actor isolation

Hello, Swift evolution!

I've been thinking about the recent discussions about default actor isolation and the #SwiftSettings macro, specifically about the idea to use a magic typealias to specify default actor isolation per file. Here are a few notable comments from other forum threads that inspired this pitch thread:

I'm definitely sympathetic to the argument that #SwiftSettings is a very heavyweight way to write something that will likely be very common in the case of default actor isolation, and that default actor isolation has a much greater semantic impact on the code than the other per-file build settings that we're considering, which are all related to diagnostic control (strict concurrency checking, strict memory safety, and warnings-as-errors / granular warning control).

I argued against the typealias direction when it came up in the pitch of SE-0466 here:

Given the feedback from the #SwiftSettings pitch, I decided to actually work through my objections. I wrote up a proposal draft here and I put up an implementation PR behind an experimental feature flag here.

I think the only mistake that's difficult to provide any actionable diagnostics before is misspelling DefaultIsolation, but I'm not convinced that'll be a common issue.

I also think that while the solution in the proposal for being able to specify nonisolated by introducing an empty type is kind of gross, the benefits of this model are pretty nice. There's no new bespoke syntax, the lookup rules are pretty straightforward, and it allows writing nonisolated consistently rather than sometimes writing nonisolated and sometimes writing nil.

Thoughts?

-Holly

7 Likes

This sounds fine to me, continuing the typealias Default... trend we have already :+1:

This is also likely to influence how we'd set the global ExecutorFactory as well. We leaned into the SwiftSettings earlier because the default isolation was doing that, so we might plop back to the typealias.

Currently the main and global executors pitch doesn't really say we'd do the settings route, and only documents the command line option and SwiftPM setting.

So follow up discussion here might be if typealias DefaultExecutorFactory = is a thing or not, and it's just the compile time flag / swiftpm setting and if so, how it'd interact if a command line option was passed as well? It might be that we don't want the typealias for the DefaultExecutorFactory but it'd feel a bit surprising perhaps, idk maybe @al45tair had thought about this already some more? We did consider the typealias a while back, but moved over to settings influenced by the other proposals in flight AFAIR.

The executor factory is global though, and not per file, so perhaps the difference here would be reasonable. We did have discussion about "only allow it in the module that has the @main" or something similar though.

Hmm, I didn't notice this bit initially and only noticed while reviewing the draft impl.

I'm not sure making this typealias a "typealias but not really, actually special syntax" is nice, I was expecting nonisolated to be spelled as nil, but this seems to be proposing:

typealias DefaultIsolation = nonisolated
// typealias DefaultIsolation = MainActor

It's unusual to be able to write a keyword in that position and makes this not really a typealias but have special syntax rules, doesn't it?

Wouldn't it be cleaner for the language to leave off special cases like that and rely on some type introduced for this purpose instead, e.g. Nonisolated? Never could be another candidate but reads a bit weird, so I'd rather introduce Nonisolated type instead probably.


edit: I noticed the implementation trick done here... It's not special syntax but a type with lowercase:

KNOWN_SDK_TYPE_DECL(Concurrency, nonisolated, NominalTypeDecl, 0)

I don't think we should lowercase that, but otherwise agree with the typealias and method of doing the setting it.

The lowercasing here though adds more confusion than it does cleanup IMHO.

Nonisolated type would be fine for this purpose, and no risk of confusing readers, especially given that syntax highlithting notoriously is broken on such sneaky tricks.

1 Like

I support the proposal and like the use of a typealias to specify the default isolation. But I'm worried about the concerns regarding the introduction of the word "nonisolated" to the type system. I feel like it can be good, but I'd like to see the whole picture with all the implications.
We don't have some beautiful symmetry between the notions of isolated and non-isolated code:

  • In function declarations, it's a modifier and an attribute (nonisolated func f() vs @MainActor func f())
  • In an isolated parameter (func f(isolation: isolated (any Actor)? = #isolation)) it's nil and an instance.
  • We don't have it yet, but it was pitched once to support the nonisolated modifier for closures.
  • SE-0461 introduces another degree of freedom to nonisolated functions.

nonisolated (or Nonisolated) as a type would add some symmetry in the proposed typealias declaration. But having it as a type may induce a desire to use it for other purposes as well, which are unlikely to be implemented. For example:

  • Use it as an attribute instead of the nonisolated modifier: @Nonisolated func f()
  • Generalize over isolation type:
    struct S<Isolation> where Isolation: /* some isolation type constraint */ { 
      @Isolation func f() { ... }
    }
    S<MainActor>().f()
    S<Nonisolated>().f()
    

Also the nonisolated type doesn't capture the changes from SE-0461. It probably shouldn't do so, but it may still be a little confusing why we can write nonisolated(nonsending) func f() async but can't typealias DefaultIsolation = nonisolated(nonsending)


Also, I have a couple of questions aside from the nonisolated type:

private typealias DefaultIsolation = CustomGlobalActor // warning: not used for default actor isolation

  1. Can we theoretically support custom global actors here?
  2. This declaration is different from regular typealiases but looks like it. Maybe we should add a prefix to "DefaultIsolation", for example, "$" like in compiler-syntesized properties? This would eliminate the possibility of clashes with existing code[1] and emphasize that this typealias is some kind of special.
    Also, this typealias will be a silent noop on older compilers, which is not the best behavior. typealias $DefaultIsolation = ..., on the other hand, will be an error.

If the answer to the first question is "No" and we will eliminate the possibility of clashes, I'd suggest promoting the warning to error.


  1. I didn't find any results on GitHub by searching for "typealias DefaultIsolation" though ā†©ļøŽ

2 Likes

Creating a new empty type for this purpose whose name doesn't meet the expected API design guidelines feels a bit odd to me. I think its existence would "imply too much", by privileging this specific keyword in a way that mainly works because it's a contextual keyword and so it's legal to use it as a type name in this position.

I would advocate for reusing Never here instead, because the existing meaning of the type feels like it could be extrapolated to serve this purposeā€”"what actor are declarations isolated to in this file? They're Never isolated by default".

I disagree with this. There's prior art for using undecorated file-/module-scoped typealiases in Swift:

  • The default types of literals that aren't otherwise inferred as a specific type can be changed by using aliases like IntegerLiteralType and StringLiteralType.
  • The default distributed actor system can be changed using by declaring DefaultDistributedActorSystem.

If we're using the typealias approach (which I support), we shouldn't use a different syntax here than we've already used for the others. The $ prefix is also reserved for synthesized declarations, which this is not. Its meaning is special but it's not synthesized by the compilerā€”the user explicitly declares it manually.

4 Likes

This pitch follows SE-0466 on the decision to not support arbitrary global actors. It's feasible and the restriction is artificial; it's not supported because I do not think it's a good idea. It's definitely a fair point that if writing an arbitrary global actor as the underlying type of DefaultIsolation is only a warning, then allowing it later will be a source breaking change.

I agree with @allevato 's counter points to this here:

I'm not too concerned about DefaultIsolation compiling with no effect when building with older compilers. Using nonisolated as an underlying type wouldn't compile anyway when building against an older standard library, so it's only private typealias DefaultIsolation = MainActor which will compile and have no effect. If we think this is a serious concern, we might want to consider introducing a bespoke syntax for this, which would solve that problem because the new syntax would only compile with newer compilers.

I agree that introducing an empty type called nonisolated is kind of a hack and exploits the fact that nonisolated is a contextual keyword. However, I really don't want to introduce a third way to spell nonisolated (e.g. nonisolated, nil, and Never/Nonisolated), even if we can construct a reasonable explanation behind each one. I'd love to stay in a world where people write nonisolated except for the case where they need to use a value to represent the isolation, e.g. when passing an argument to an optional isolated parameter (and I think in the SE-0461 world, nil as isolation specifically means @concurrent).

I also don't think that leaning into a proper type (that looks like a type via naming conventions or using a standard type) is better here, because this typealias isn't used as a normal type like the other cases of magic typealiases. What programmers are specifying is the isolation attribute or modifier to use in front of declarations with unspecified isolation.

1 Like

I agree with the justification for using lowercase nonisolated. The fact that itā€™s technically a type is an implementation detail, and forcing it to look like a type would just be leaking an implementation detail. We donā€™t need to get caught up on why it actually works under the hood, and can just pretend weā€™re typing the keyword directly.

With that said, we should make sure the documentation makes sense in case you trigger the quick documentation in your IDE. Overall Iā€™m very positive about this pitch.

Could nonisolated be written as:

private typealias DefaultIsolation = (any Actor)?

to match the default #isolation type?

While I'm not entirely convinced, I'm not unconvinced either. I'm definitely finding it hard to argue that a different spelling is better than being able to just write nonisolated here, especially from a language user's (as opposed to a language lawyer's) point of view.

If we end up going with the new nonisolated type in the _Concurrency module, then the only nitpick I have is that it should probably be public enum nonisolated {} rather than public struct as the pitch currently spells it, so that users can't pass around instances of it as if it was meaningful.

3 Likes

Just to follow up on this point, the plan now is to use

typealias DefaultExecutorFactory = ...

and we will not be having a command line option. Since it's program-global, the only DefaultExecutorFactory typealias that matters is the one in the main program. If we don't find that, we fall back to the one in the Concurrency runtime.

1 Like

Hooray! I think thatā€™s very nice and itā€™s great to keep consistency between all these :smiley:

-1 on the typealias syntax.

I think such a crucial piece of information is going to be buried if it's masquerading as a typealias:

typealias MyType = ThingOne
typealias SomeThing = SomeThingElse
private typealias DefaultActorIsolationType = MainActor
private typealias SomeFoo = SomeBar

If we are going to place the burden on developers now to read file-by-file defaults, we should make it exceedingly clear up front what they are.

I'm definitely sympathetic to the argument that #SwiftSettings is a very heavyweight way to write something

What does heavyweight mean here? More pronounced? If so, that should be exactly what we are going for.

2 Likes

Dumb questions:

  1. should nonisolated be frozen?
  2. will the 6.2 availability of nonisolated cause problems (i.e. will it make our ability to specify per-file default isolation target-dependent in a way that it otherwise would not be)?
  3. the considerations in "alternatives considered" do not seem to differentiate between nonisolated and Nonisolated. Is there a reason to pick the lower-case one other than consistency with other uses of "nonisolated"?
1 Like