[Second Review] SE-0478: File-level defaults

On composition

In a lot of cases it would "do nothing", but they compose. The module feature performs inference, which this feature does not. For example, in MainActor-by-default mode, conforming to a nonisolated protocol would make your conformance nonisolated. If you write that conformance in a file with default @MainActor, then it is as though @MainActor was written on the conformance, overriding the inference rule and potentially leading to an error (which alerts you that you wrote something impossible). In Swift 5 + MainActor-by-default mode, default @MainActor would apply @MainActor without @preconcurrency, while the module default would have @preconcurrency. Note that this is the same behavior as writing the annotation explicitly in this configuration.

If you are aware of behavior this wouldn't cancel, I'm extremely interested to hear it! My intention and belief is that it would cancel out all the behaviors of the flag besides the other feature flags (NonisolatedNonsendingByDefault and NoExplicitNonisolated) that MainActor-by-default enables (turning off other associated feature flags per file would cause issues for adoption and unadoption, as well as being extremely surprising in my opinion).

[Second Review] SE-0478: File-level defaults - #17 by xwu looks correct, that reflects my intention with the proposal, and divergence from this in the implementation would be a bug.

Should we allow default anywhere in a file?

I'm not confident we can rule this out, so it seems to have some (minimal) value to leave this door open. If we discovered a need to do this in the future but allow default anywhere in the file, we would need to invent new syntax, or conditionalize the behavior of default declarations based on their contents. However I'm personally opposed to a #pragma style feature that can be toggled throughout the file, since I feel it makes local reasoning harder.

I agree! Swift is very amenable to splitting things between files, so I see no issue with this.

What is the argument for allowing a file default to be written anywhere? We can support a fix-it for moving the default declaration to the top of the file. I would even prefer it was not allowed after import, but I was told that would be onerous. In my mind, requiring file defaults to be written at the top of the file means "is there a default somewhere else" is never a question you need to wonder about when debugging, even when you are using a plain text editor. When I open a file, I see what defaults it has right away. For IDE features like jumping to definition or PR review this is less applicable... Yes, the compiler can understand it anywhere, but why is it ever desirable to allow writing a file level default in the middle of a file? My position/bet is that no programmer would feel hindered by needing to write file level defaults at the top of the file. If we discover that is wrong, we can always lift this restriction later, but we can never introduce it without staging and migration. Is it good that import statements can be written anywhere, beyond empowering more confusing code?

Is this desirable? Maybe the consensus is that Swift is already like this, so there's no reason to try to prevent it for new features?

I don't think forbidding file level defaults below the top of the file is necessary, but I think it's nice.

On language dialects

NonisolatedNonsendingByDefault and NoExplicitNonisolated are not intended to remain as dialects, there just needs to be staging and care there to not break source compatibility. See SE-0461 and this reply from Holly. -default-isolation is a permanent dialect, which I personally think is slightly unfortunate.

However I'd argue file-level default isolation is not a dialect; it's something that can be freely adopted and unadopted per-file!

IDE experience

This should be the case with my current PR! File level default isolation is treated as another kind of inferred isolation (with the rule of applying to every top level declaration lacking an annotation), applied after explicit attributes but before other inference rules, so any editor that is able to show the type of a decl with an isolation that wasn't explicitly written (via an inlay hint, hover to show type, color coding, whatever your editor does already) should apply to file defaults. I personally agree it would be extremely valuable for Xcode to show inferred isolation as an inlay hint if it doesn't already.

The only order sensitivity is priority. No rules exist today prohibiting repeated @available or @diagnose on a declaration, and the order determines which wins. Which default @available wins is determined by order in the same way:

default @available(*, deprecated, message: "first default")
default @available(*, deprecated, message: "second default")

// CHECK:      @available(*, deprecated, message: "first default")
// CHECK-NEXT: @available(*, deprecated, message: "second default")
// CHECK-NEXT: public func defaultedOrderedFunc()
public func defaultedOrderedFunc() {}

// ...

func useOrdering() {
  defaultedOrderedFunc() // expected-warning {{'defaultedOrderedFunc()' is deprecated: second default}}
}

We can add a warning for this case if we add a warning for repeated available attributes more generally.

On inference, and being different from -default-isolation

They are very different features! The module default is a language dialect that tries its best to infer the intention of code without annotations and get it to compile. It is intended to make it easier to write mostly single-threaded things, and even if you feel it doesn't achieve this, it certainly does better than file-level default isolation at this purpose. A file-level default isolation, as proposed, attempts to apply your specified default to every single declaration where it is coherent, and give you an error when it causes issues. This would be a very poor experience for writing single threaded code that wants to interact with external functions and protocols. These two cases not being the "same kind" of default personally biases me back to using.

Do we need this?

While it doesn't exonerate a new feature, Swift is full of features that can be used to write both clean and illegible code. Powerful but risky features like operator overloading are eschewed in other languages but present in Swift, and it comes down to taste and convention to use them well.

I think the strongest argument for a feature like this regarding readability is highlighting the novel case; in a file where all but one declaration is nonisolated, the repeated nonisolated keyword becomes noise. Using default nonisolated, the one @MainActor declaration is highlighted. If something is actually boilerplate, it can improve readability to eliminate it. In a case like @unsafe, littering it throughout the file might instead be a smell and not something to eliminate.

I feel this is another tool in the toolbox for writing clean code. Programmers can use it where it is appropriate, and be explicit when that is more readable--just like other features that can hide information, like type inference! I see it being used primarily by -default-isolation MainActor projects for a couple files that implement concurrent types and algorithms, to isolate a file of UI state to @MainActor, and to be explicit about assumed inferred isolation.

Existing typealias based defaults

I think it would be great to create attribute forms of the existing typealias configurations, and support them as file-level defaults! I forgot to mention distributed actors, but they could potentially also benefit from treatment like that in the future (if we defined an attribute syntax for them).

My feelings on syntax

I agree that it's clean! I was more excited about it when the pitch had file-level defaults and module-level defaults behave in the same way, and I do wonder if using or similar is worth consideration given that they are not the same "kind" of default. While no syntax choice will be fully explanatory without being overly verbose (file-level-apply-to-all-decls-default-isolation @MainActor) choosing using could make it clear that default isolation does something different from the language default. It is worth noting that we can implement using without any source compatibility issues since you can't write top level code like this proposal uses today. If we want using { ... } or using( ... ) in the future I suspect we will run into a bit of trouble.

2 Likes