Pitch: Fully-Qualified Lookups

Fully-Qualified Lookups

Introduction

We propose a syntax for unambiguously referencing any symbol, regardless of local shadowing.

Motivation

Swift currently lacks a way to reference symbols in the global namespace that have been shadowed by closer declarations. The following example illustrates the issue:

struct Swift {
    // This is a custom struct that shadows the implicitly-imported Swift namespace.
    // While naming a struct `Swift` may be inadvisable, other names are not reserved.
    // Any two modules might choose to export structs with the same name.
    // Importing both in one file will cause one to shadow the other.
}

func dump(_ thing: Any) -> Void {
    // This is a custom function that clients call to produce extra information when dumping objects.
    print("Custom dump function called!")
    
    // This causes infinite recursion, because `func dump()` shadows `Swift.dump()`.
    dump(thing)
    
    // This causes a compiler error, because `struct Swift` shadows the implicit `import Swift`.
    Swift.dump(thing)
}

Proposed solution

We propose the prefix _. to force fully-qualified lookup. This prefix is the Swift analogue of C++’s scope resolution operator :: when used as a prefix.

When Swift encounters an identifier expression that begins with _., the portion of the identifier expression following the period will be looked up in the unnamed global namespace. The global namespace consists of the following identifiers:

  • The names of all imported modules, including the implicitly imported Swift module.
  • The reserved name Self, which contains all top-level identifiers visible from the file being compiled.

The Self identifier allows clients to work around declarations which shadow top-level declarations, without having to build the name of their module into the source file. For example:

private func isGreater(_ a: Any, _ b: Any) -> Bool {
    return false
}

public enum MyHelpers {
  static func isGreater(_ a: Int, _ b: Int) -> Bool {
      return _.Self.isGreater(a, b)
  }
}

Detailed design

The postfix-expression production in the grammar gains a new alternative:

postfix-expression → fully-qualified-identifier-expression

The fully-qualified-identifier-expression production is defined as follows:

fully-qualified-identifier-expression → _. explicit-member-expression

Source compatibility

This is a source-compatible change. The _. prefix is currently not valid in any expression, and the use of Self does not conflict with any existing uses.

Effect on ABI stability

This proposal does not affect ABI.

Effect on API resilience

This proposal does not affect API resilience.

Alternatives considered

@beccadax has previously opened up a discussion with several ideas, such as @qualified, #Modules, and ::. To my knowledge, this discussion did not address the issue of shadowed top-level declarations.

7 Likes

This is an important problem to solve. I haven’t yet thought through how the spelling will look and feel in practice, but I have a nit to pick with the introduction:

This is not, strictly speaking, accurate, as there will remain unutterable symbols in nested scopes:

func foo() {
  let x: Int = 1      // (1)
  
  func bar() {
    let x: Double = 2
    
    func baz() {
      let y: Int = x  // error, cannot reference (1) here
    }
  }
}
5 Likes

Good point. I don’t think it’s necessarily worth solving the “locals shadowing locals” problem. You can always refactor your function to name the locals differently; you can’t always resolve a name conflict from two upstream imports.

5 Likes

This also doesn’t solve the problem of disambiguating members by module, but I think that’s fine too! “Any top-level name” or “any module-scope name” would describe this fine.

It’s a useful feature, I don’t love this spelling (underscore signifies an inferrable gap in types today), but ultimately that’s not the important part, so I’ll trust y’all to come up with something better or demonstrate that this is the best choice by the time it gets to review. The general semantics of “a namespace containing only module names and Self” sounds correct to me.

4 Likes

Are you referring to something like two separate modules that each define an extension on a type imported from a shared third module?

underscore signifies an inferrable gap in types today

IMO, it currently means “black hole to assign discardable results to” :wink:

1 Like

I agree. I think it's an important feature, but _ does not feel right, as it already has specific uses in Swift and also other languages. I think a reserved name would be good. Scala for example uses _root_.

2 Likes

Any reserved name that is a valid Swift identifier makes this a source-breaking change.

_ already had a specific meaning in Swift (an unnamed identifier that can only be used on the left side of an assignment operator) when it was also bestowed the meaning of “inferrable hole in a type signature”. I think the horse is out of the barn on giving _ multiple meanings. In this context, it means “the unnameable root namespace”.

The underscore also signifies an unnamed function parameter, from the perspective of the caller.

IMO the best way to solve this problem is to create a module keyword.

(module Swift).dump(thing)
3 Likes

How about: if there's a module name with the same name then to reference the type you'll have to enclose it in ticks:

Swift.dump() vs `Swift`.dump()
1 Like

Backticks exist to spell identifiers that overlap with reserved words or contain special characters. They can’t help disambiguate between the module `default` and the top level struct `default` { }.

3 Likes

Yes, I was not advertising to use _root_ specifically, just wanted to give an example.

Yes, there are multiple uses of _ in other languages as well, but they usually have one thing in common: it refers to something unspecified, like the explicit assignment to no variable, a type hole, or explicitly not naming a parameter. In this proposal, it would mean something very specific: the namespace root. We could for example prefix it with $, which is reserved for synthesized declarations in Swift.

Because $ refers to the underlying storage of a wrapped property, it cannot be used to create a source-compatible identifier for the root namespace.

Ah, yes. I did not think about that.

I mean we can teach ticks new tricks.

Indeed they won't. I'd not start module name with lowercase character anyway.

I remember the case when I was not able naming my type State - very strange things happened when I did. I learned not to do that and moved on. I wouldn't be surprised if I also can't name my module Type or class or nil, or false, etc

But backticks cannot both escape a reserved word and disambiguate that reserved word at the same time. And I don’t think your alternative replicates the _.Self functionality from my pitch.

My understanding is that the very purpose of the backtick syntax is to enable all of these things. If any of these aren’t allowed, I would file a bug.

2 Likes

I don’t dislike this alternative, but I do worry about the number of context-sensitive keywords we’re adding, such as any Foo and some Foo. I recall this being a thorn in the side of the C# team.

Couldn't it be used in place of the pitched _?

E.g. like this:

private func isGreater(_ a: Any, _ b: Any) -> Bool {
    return false
}

public enum MyHelpers {
    static func isGreater(_ a: Int, _ b: Int) -> Bool {
        return $.Self.isGreater(a, b)
    }
}

Maybe there is an even better spelling but I think that _ has too much different meanings in Swift already.

2 Likes

To be very pedantic, _ had the original meaning of “ignore” pattern, usable on the left-hand side of an assignment or in a case. Parameters were also patterns at one point, but now they’re not and the _ there is not quite the same thing.

The second use of underscore was for parameter labels that can be omitted at the call site (they still show up in a function’s full name). I can’t really say this is the same meaning as the original one.

The most recent use of underscore is in types, to request inference. That’s the one I brought up because it’s closest to conflicting with this proposal; if we ever wanted to support inferred base types and not just inferred generic arguments, that would be most analogously spelled _<Int>, as in let x: _<Int32> = [0].

11 Likes

To go even further: though it isn't supported by the implementation of placeholder types today, the syntax here has at least a very plausible reading as a placeholder type—specifying an "outer" type that should be inferred, while the "inner" type name is fixed:

struct S {
    struct Nested {}
}

struct R {
    struct Nested {}
}

let rn: _.Nested = R.Nested()
4 Likes