[Review] SE-0090: Remove .self and freely allow type references in expressions

Personally, I'm in agreement with @crontab that the problem of disambiguation can be solved by assuming a type first which is aligned with real-world usage. In the case of an Array literal at least the fix is to just be more verbose in typing the Array's type.

3 Likes

Even though [Int]() is "normal" Swift, it definitely causes some grossness in the parser and type checker. That doesn't parse as a type at all; it's parsed as an array literal containing a single element Int, and a function call applied to that. The type checker then looks for expressions like that and converts them to types before solving.

If we allowed users to write Int instead of Int.self, then consider this example that becomes ambiguous:

extension Array {
  func callAsFunction() { ... }
}
let x = [Int]()

Is this calling an initializer on Array<Int>, or is it constructing an array with a single element whose value is the metatype Int.self and calling it as a function application?

Sure, it's unlikely that anyone would write that code, but the type checker has to consider the possibility. Any time you say "the problem of disambiguation can be solved by assuming X", you need to show that adding "X" to some disjunction isn't going to cause a performance problem.

9 Likes

@allevato thanks for illustrating the valid case where this, yes, isn't easy to write away as solvable by just assuming type-first.

That specific example, where extension Array { func callAsFunction() {} } and [Int]() might mean array-literal-then-function-call, to me feels like it sits at the absolute fringes of what’s possible in Swift.

Swift IMO as a language has never let super-improbable fringe code hamper everyday usability.

Swift already does far more intricate "overload resolution" and expression rewriting, a few features that come to mind:

  • Trailing closure syntax
  • A ton of operator disambiguation scenarios (optional chaining vs postfix operators for example).
  • Even type inference of something like Set([1, 2, 3]) where it is inferred correctly as Set<Int> instead of Set<Array<Int>>.

I might be entirely wrong on the complexities of the implementations for the scenarios that I mentioned, so please forgive my ignorance here - would love to know I could be thinking clearer here.

2 Likes

To be concrete, the proposal was deferred because it was uncertain whether it was implementable. To resurrect this proposal, an implementation that is sufficient to demonstrate that it is possible to overcome the implementation-level concerns raised by the core team would need to exist. Discussion in the abstract is unlikely to move the needle here.

9 Likes

I'm not familiar with Swift compiler's inner workings, and having built a compiler once, I'd think it should go like this:

If there's an opening square bracket then:

  • if it's in the context of array of type references then parse what follows as a list of type references
  • else if the first expression is a type, then expect a closing square bracket and treat it as a reference to Array<*type*>
  • else parse the rest as an array literal

Does it make sense? Or is Swift more complex than that?

To this, I would point to SE-0220: count(where:), a seemingly simple addition that was pitched and accepted all the way back in 2018 but didn't truly land until Swift 6.0 specifically because there were type checker performance issues that had to be solved before it was deemed usable. And that's not even involving fringe code.

Parsing happens before semantic analysis. You can't make decisions based on whether an identifier references a type or not at parsing time, because that isn't known yet.

6 Likes

Touché. That particular count(where:) example makes me sad :(

I think required metatype .self feels weirder now that identity key paths finally started working last year.

Enum.self[keyPath: \.self] // Compiles, as it should.
Enum[keyPath: \.self] // Expected member name or initializer call after type name

I see the argument about array literal syntax. If that's not involved, I don't think extra .self helps people.

func takeArguments<each T>(_: repeat each T) { }
enum Enum { case `case` }
takeArguments( // All of these compile.
  Enum.self.case.self,
  Enum.self.case,
  Enum.case.self,
  Enum.case,
  Enum.self
)
f(Enum) // Expected member name or initializer call after type name

Am I wrong to feel like the main usage of .self is metatype arguments? I'm open to it being considered disambiguating elsewhere, but I really don't think it does anything to help mentally parse arguments. The compiler won't let you try to use a nested instance of a metatype when defining an instance of an outer metatype.

// This all compiles, as it should.
func f(_: (some Any).Type) { }
f([Int].self)
f([Int.Type].self)
_ = [Int.self].self as [Int.Type]

f([Int.self].self) // Cannot convert value of type '[Int.Type]' to expected argument type 'some Any.Type'
1 Like

Pull requests welcome ;-)

3 Likes

So today, type expressions are folded in the “pre-check” pass that runs before constraint solving, so in this case we don’t actually consider both possibilities. But the current behavior is also wrong in other ways, for example ().self is interpreted as the empty tuple value and not the empty tuple type—to get the latter you have to refer to a type alias, for example Void.self.

5 Likes

What year is it!?

I'm in favour of allowing .self to be elided. It would make some of Swift Testing's macros simpler (because we wouldn't need to conditionally insert self, and wouldn't need to try to guess if it's needed.)

I believe E.self is always allowed for every expression E (it evaluates to the same value as E), so you could just always insert it, too.

1 Like

TIL! What was the background for allowing literally any value to have an implicit .self property? That feels like a gift that keeps on giving, and ".self" is kind of a hard thing to search for in the archives.

FWIW, if it's possible to always prefer the type interpretation in the case of my tortured callAsFunction example instead of it being a bottleneck, I'd be all for that because literally nobody should ever write that code.

I believe this was inspired by Objective-C, where the NSObject class responds to self by returning self, but I’m not sure because this was before my time on the team. However note that it’s not a real property in Swift, just built-in syntax.

1 Like

We mostly add .self indiscriminately for that reason, but there are caveats:

  • Rewriting the relevant syntax tree impacts compile time; and
  • We sometimes need to deal with constructs like Foo.bar which works fine rewritten as f(Foo.self).bar.self… unless Foo is a module name. f(Foo).bar would still be invalid unless modules suddenly became symbols in their own right, but this proposal would presumably be a step in the right direction.
1 Like