SE-0491: Module selectors for name disambiguation

This kind of shadowing comes to mind:

/// module X
protocol P { }
extension P { typealias B = String }

/// module Y
protocol Q { }
extension Q { typealias B = Int }

/// module Z
func f<T: P & Q>(_: T) {
	var x: T.B // type is ???
}

That said, this issue can happen with everything in a single module too, so using module for disambiguation won’t necessarily work. With some experiment (keeping everything in the same module) it looks like the compiler resolves this by picking the first protocol with a B type in it, in alphabetical order of the protocol name.

6 Likes

Happy for a solution, but really hope :: is not the final syntax. Its precedent in C++, a language that reads like symbol soup, should raise some eyebrows.

Let’s look at some examples from the “Future directions” section:

_::ignite()
*::ignite()
myArray.Swift::[myIndex]

That’s three non-alphanumeric symbols in a row!

As Review Manager I'd just like to prompt: while I realize it's not always easy, responses of the form "I don't like syntax X because..." are far more actionable and helpful if they include discussion or explorations of concrete alternatives. A good place to start would be the Use a syntax that avoids ::'s shortcomings section under Alternatives considered—is there a particular alternative there that you think fares better, or for which you disagree with the reasoning for rejection? Or: is there a potential alternative not mentioned in Alternatives considered which you you think addresses all the shortcomings of alternatives considered thus far?

9 Likes

IMO The Use a syntax that avoids ::'s shortcomings didn’t do enough to adequately explore other syntaxes. Here’s one that (to me) is markedly clearer:

Mission.(NASA)Booster.Exhaust   // Arbitrary; little connection to prior art

There is indeed prior art here, from the English language!

According to Gemini:

Parentheses are used to enclose information that provides clarification, explanation, or additional detail to the main sentence, such as acronyms (e.g., ESP), parenthetical comments, author-date citations, or to list items in a series…

Parentheses are used to clarify, and this feature is about clarifying which module a name comes. This reads so much like English:

Mission.(NASA)Booster.Exhaust
Mission.(CNSA)Booster.Exhaust

I personally don’t view prior art from C++ as a selling point. I believe parentheses have sufficient inherent understanding as a clarifier from human language that this is the better option.

6 Likes

Thank you for elaborating!

That's strange, I think it should be diagnosed as an ambiguity. Do you mind filing an issue?

Type aliases in protocols are a bit funny in general. If you move them from the extension into the protocols themselves, it changes the meaning of the program and we reject the declaration of f():

4 | func f<T: P & Q>(_: T) {
  |      `- error: no type for 'T.B' can satisfy both 'T.B == Int' and 'T.B == String'

3 Likes

Sort of.

If -> is , which we take it to be, then :: is . I.e. it's a bigram masquerading as a monogram.

I parse it as the vertices of a giant pixelated (and unfortunately somewhat rectangular) dot operator (Module⏹property), but it would be nice to have a pronounceable name for whatever it's actually supposed to look like. (Like how the documentation calls -> a "return arrow".)

Note: this post doesn’t render correctly if not on macOS 26.

1 Like

These are all examples of things which are not being proposed.

3 Likes

If the request was to propose something other than ::, I think that I might suggest @ as that should be representable on all file systems addressing the serialisation issue while still being concise. The problem is, I’m uncertain about the implications for parsing with older compilers.

I know that had a similar issue (however, that is something that I would probably rank as better than @ as there is prior art for that - Windows uses as the module separator for stack traces).

However, I wonder if the parsing issue is truly a concern as a flag could be used to enable the feature and then be made default in a future language version.

2 Likes

Ah. I just saw the bad news about subscripts. I don't think it makes sense to exclude them. There's already precedent for the above syntax:

struct S {
  static subscript(_: Int) -> Void {  }
  subscript(_: Int) -> Void {  }
}

let myIndex = 0
_ = \.[myIndex] as KeyPath<S.Type, _>
_ = \.[myIndex] as KeyPath<S, _>

I don't think it would make sense not to allow

\S.Type.MyModule::[myIndex]
\S.MyModule::[myIndex]

And, this follows:

S.MyModule::[myIndex]
S().MyModule::[myIndex]

There's no similar precedent for callAsFunction yet, but I think it ought to work similarly.

struct S {
  func callAsFunction(_: Int) { }
}

let s = S()
s(1)
s.MyModule::(1)

callAsFunction can at least be referenced using the name callAsFunction; that was one of the reasons it was made a named function with special meaning instead of a completely new kind of declaration. I don't think we need to or should invent a separate custom syntax in addition to that—I would expect this to work:

let s = S()
s(1)
s.MyModule::callAsFunction(1)
6 Likes

@ is not a great choice in general because when the parser is parsing an invalid declaration, @ is one of the tokens it looks for to recover by finding the start of the next declaration. Allowing @ to appear in the middle of an expression would make this heuristic less effective.

To address your original concern about DocC filenames:

  • Module selectors are used when writing a reference to a declaration in source code. They are never part of the canonical name of a declaration, so I don’t think there would ever be any reason for DocC to write them in a filename.

    • In theory, module selectors might be useful in the filenames of extensions to imported types, but in practice, DocC is already using a nested directory for this.
  • Even if there were, : can already appear in the compound name of an initializer, method, or subscript and DocC needs to handle that; it could do the same thing for module selectors if it never needed to use one.

    • Testing on macOS seems to suggest that DocC just…puts : in the filename when you use a compound name. I don’t know if there’s a workaround for Windows or not.
1 Like

We’ve already had some discussions about how DocC just isn't set up to respect the Windows rules about filenames: Unsafe characters in file names redux

Issue filled.

But it makes me wonder if this :: operator will be usable eventually for disambiguating between those typealiases coming from the same module. It’d be unfortunate to have to invent a completely different syntax for that.

protocol P { }
extension P { typealias B = String }

protocol Q { }
extension Q { typealias B = Int }

func f<T: P & Q>(_: T) {
	var x: T.B // type is ???
	var y: T.CurrentModule::B // type is ???
	var z: T.P::B // no module P
	var a: T.CurrentModule::P::B // type is String (yeah!)
}

I suppose that’s a question for another proposal down the line, but it’d be nice if there was enough room left to disambiguate this.

I agree with what you've said here and would add this: raw identifiers (SE-0451) make it possible for just about any character to appear in an identifier that might end up generated in documentation. Therefore, tools like DocC—or any other tools that produce filenames based on identifiers or decl-names—are the ones who should be responsible for sanitizing/transforming the identifiers to be compatible with all the supported file systems. We should certainly not be designing language features based around what's compatible with various file systems.

15 Likes

The proposal mentions that using :: to disambiguate protocol names would re-introduce an ambiguity between types and modules.

The issue you describe is not specific to type aliases and can happen with any kind of protocol extension member, so perhaps it’s unavoidable that one day we’ll need a special syntax for that too.

However, in this case, the underlying types of your type aliases do not involve Self, so in fact we allow them to be referenced as members of the protocols themselves, ie P.B and Q.B.

1 Like

This is what prevents DocC from being available on Windows today. This would make it even harder to make the project work on Windows. I think that it is a valuable enough trade off - would avoid this issue and a flag would allow the proper handling.

There has been a pending change to try to alleviate this issue pending for ~2 years now that has not been possible to merge.

The proposal discusses this as a general principle in the Separate modules make this uniquely severe section. tl;dr: When two declarations in the same module conflict, we expect its maintainers to notice the conflict and redesign their module to avoid it. We don’t expect that for declarations in separate modules because it’s entirely possible that only the client is aware that both declarations exist, so clients need tools to resolve those.

Again, I don’t anticipate DocC ever needing to print module selectors in filenames, and if for some reason it ever does, it can apply whatever escaping or substitution it eventually develops for the colons in compound names to the colons in module selectors too.

I’m sorry that it has been so difficult to form consensus on a solution to the DocC filename issue, but I don’t think it’s relevant to this review.

9 Likes

Rather than introducing a new syntax for a type expression, what we really need is a sentinel to make it immediately obvious that a name that follows the sentinel is a module name.

Given the type expression:

Foo::Bar

The infix sentinel :: does not make it immediately obvious that Foo is a module name, especially in cases where the module name is more than seven characters long.

SomeLongName::Bar

The prefix sentinel ::, however, would make it immediately obvious that SomeLongName is a module name.

::SomeLongName.Bar

Although I am still a C++ fan, I would prefer that a different sentinel were used for this purpose in Swift.

For example, @:

@Foo.Bar
1 Like

The solution is supposed to disambiguate member names too; it isn't just "do the root lookup here, which is always a module". You could be proposing foo.@SomeLongName.bar(), but I wasn't reading your comment as that.

2 Likes