Ban generic parameter direct shadowing in type & extension declaration contexts

This proposal advises disallowing a special case of generic parameter shadowing in type and extension declaration contexts: a nested type declaration that shadows a generic parameter of its immediately enclosing type or extension declaration will become a redeclaration error.

Motivation

Shadowing rules in Swift are able to be rather loose due to its well designed grammar and type system. Almost any nominal declaration can be shadowed, and this also holds true when shadowing has no practical value.

let number: Int? = 0

if let number = number {
    let number = "0"
}

In a sense, the special case this proposal focuses on stands before the roadmap of the language, but it can also hardly be called a reasonable shadowing rule in Swift: the generic parameter and the shadowing type share the same declaration context.

Today, generic parameters can be shadowed with type aliases or nested type declarations. For the current declaration context, this means occurences of the relevant identifier will resolve to the new type rather than the generic parameter. In function and local contexts only subsequent occurences are affected:

func foo<U>(arg: U) {
	let x = arg // OK
	typealias U = Int
	let y = arg // Cannot convert value of type 'U' to specified type 'Int'
}

However, things are different in the body of a nominal type or extension declaration, where forward referencing is allowed. Not only all ocurrences are affected, but the shadowing also happens and can be realized retroactively.

struct Box<T> {
    let value: T
    typealias T = Int
}

let box = Box<String>(value: "Hello World") // Cannot convert value of type 'String' to expected argument type 'Int'

Diagnostics

Diagnostic messages are intended to help a user track down the source of the error. Allowing for shadowing of type identifiers within a single declaration context in a model to which that is alien results in misleading and confusing errors.

extension Box {
    func copy() -> Box<T> {
        return Box(value: value) // Cannot convert return expression of type 'Box<T>' to return type 'Box<Box<T>.T>'
    }
}

Furthermore, shadowing a generic parameter unintentionally through an extension might not affect other code and remain unnoticed until some of that code is put to use. This can happen if a developer declares a nested type or type alias that turns out to match a generic parameter. Where the majority would rely on a redeclaration error, even an error is not guaranteed. In fact, a type lookup ambiguity only occurs when extending types from a precompiled library. Granted, this scenario is fairly rare, but must be considered as part of a bigger picture.

Retroactive implications

Shadowing a generic parameter retroactively can break code. This is clearly a breach in the established resilience model that strives to keep clear of mechanisms capable of retroactively breaking source. We cannot extend enums with new cases or protocols with requirements for that very reason. Occasions with shadowing sometimes force the compiler to magically ignore technically correct albeit harmful code when the target declaration source is inaccessible to the consumer, i.e. an SDK framework.

struct Box<T> {
    let value: T
    
    func toArray<U>() -> [Box<U>] where T: Sequence, T.Element == U {
        return value.map { Box<U>(value: $0) }
    }
}

extension Box {
   typealias T = Int 
   // Breaks 'toArray' and any other previously declared API of 
   // 'Box' that doesn't expect 'T == Int'.
}

When shadowing occurs directly in the context of the generic type, it is exceedingly hard to come up with a use case that could serve a compelling argument in favor of having such a possibility. But the main reason to consider this proposal isn't just a matter of practicality or correctness. The concrete shadowing rules described above are also a hindrance to the future of the language. There is a number of wanted features that will affect this issue if implemented:

  • Accessing generic parameters through qualified lookup. In the event of adoption, cases that fall under this proposal will be subject to conflicting accesses and hence shadowing effectively becomes a violation.
  • Allowing static properties on generic types. Static properties also give room to analogous conflicts that naturally produce redeclaration errors elsewhere.
    class Class {
        class Foo {}
        static let Foo = 0 // Invalid redeclaration of 'Foo'
    }
    

Proposed solution

Change the shadowing rule so that it becomes a redeclaration error. Although generic parameters currently cannot be accessed from outside its type, they are technically part of the type's declaration context.

Source compatibility

Code that relies on these shadowing occasions will no longer work. However, because there is next to no reason to use this possibility, the impact is expected to be minor.

Effect on ABI stability

None.

Effect on API resilience

None, either than eliminating a non-resilient shadowing rule.

5 Likes

Glad to see you‘re picking on some parts of my previous pitch. I‘m totally support this one.

What are the “next to no” reasons for which one might rely on this feature?

Insurance in case they do exist. I couldn't think of any myself, although I had some quick thoughts on how to bind an associated type to a matching generic parameter if associated type inference were completely removed, but that's very unlikely and it would still be self-referencing.

This proposal would break the following code:

protocol A {
  associatedtype B
}

struct C<B> {
  struct D: A {
  }
}

In that code, the associated type for D.B is inferred to be C.B, but there's currently no way to spell this outside of associated type inference. You can't write typealias B = C.B because you need to explicitly spell out C's generic parameter...which is B. This compiles with associated type inference enabled, however.

I think that is a bug, and I'd like to see fixed, but I don't know a good way to spell a typealias for B that makes sense.

Edit: It's also worth noting the standard library does this. In that example, associatedtype inference is inferring the typealias for Key and Value, resolved to Dictionary's Key and Value.

2 Likes

Binding to an equally named generic parameter is an essential part of associated type inference, at least when that inference doesn't expand to outer scopes. But why would the proposal break that code? I don't see any shadowing at all.

The issue is the implementation of associatedtype inference: it synthesizes a typealias that itself shadows the parent generic type.

Isn‘t this due to missing qualified lookup on generic types?

The current pitch takes a few ideas from that thread. I could probably write a proposal but it‘s out of my scope to be able to implement it. With qualified lookup you‘ll be able to write a typealias on your nested type to refer to the generic parameter of the outer type.

1 Like

Ah, right. We won't touch synthesized code of course, especially since there's currently no other way to bind an AT to a generic parameter. When qualified lookup expands to generic parameters, we can get rid of that internal shadowing as well. I'll add a paragraph about associated type inference to elaborate on that.

In the simple case, is it internally spelled as typealias B = Foo.B or typealias B = B? Either way it would also be self-referencing without compiler support.

When I stumbled upon the same problem in your pitch, there were 2 main reasons I decided not to got for the whole thing with you.

  • The shadowing problem is related, but can be done separately. There's a higher chance to land a compound pitch at least partially step by step in terms of implementation.
  • Unless it's something probably never mentioned before, I try to pitch problems I am likely capable of implementing myself in reasonable time. Besides, I find it easier and more efficient to gain experience incrementally without wasting months puzzling on something I'm frankly not ready to tackle.

If this proposal has a positive outcome, we can safely move on to qualified lookup. And thanks to this proposal, I may be able to help you realize it.

You may have misunderstood my intention. I‘m totally up for bringing these change in smaller parts. I‘m also not against this pitch. All I was trying to say that the mentioned issue can be handled if we had qualified lookup today ;) Nothing more nothing less.

The only thing I‘m worried about that qualified lookup probably won‘t happen before Swift 5, and since it is kind of includes breaking changes I‘m not sue if we can ever get that feature in Swift post ABI stabilization.

I think @Douglas_Gregor could answer the question related to the addition of qualified lookup in post ABI stable Swift better.

One thing I have to mention though. If you‘d have qualified lookup then theoretically you‘d also ban the shadowing of the generic type parameters because it would result in a invalid redeclaration error, don‘t you think?

For example in RxSwift they define a typealias because of missing qualified lookup you don‘t inherit the generic type parameter in a subclass.

If you‘d try to write another typealias named ˋEˋ you‘ll get a redeclaration error.

No, I wasn't arguing, but I see how it could come off that way.

Yes, that's why I mentioned qualified lookup as a feature that requires eliminating this shadowing rule in explicit code.

1 Like

Changes to name lookup affect the source language, but will have no ABI impact.

Doug

3 Likes