SE-0491: Module selectors for name disambiguation

Prefix @ is not a viable option because it's ambiguous with a custom attribute.

@IsThisAFunctionCall.OrIsItAFunctionBuilderOnFn(...)
func fn() { ... }

More broadly, I'm uncomfortable with designs that put a . between the module name and declaration name because, particularly in a qualified lookup, the relationship between the module selector and the name after it is different from the relationship between other names separated by dots.

TheFirstThreeDots.lookInside.thePreviousMember.::ButTheLastOneDoesnt.itQualifiesThisName

It seems to me that the special connection between ButTheLastOneDoesnt and itQualifiesThisName should be conveyed by using a different symbol to link those two.

2 Likes

I really like this suggestion. Has this been discussed before? Will this work with the parser?

I don’t think it’s been seriously discussed. Some thoughts on parsing:

  1. You can't tell whether (NASA) is a module selector or a parenthesized expression/type until you know whether the token after the closing paren is an identifier or not. This is three tokens past the open paren, where you would ideally make that decision. While the parser is capable of looking an arbitrary number of tokens ahead when it needs to, looking more than one token ahead is more complicated and expensive.

  2. You won’t be able to divide the sequence with a line break after the parentheses:

    (NASA)
      Mission    // Ambiguous; could mean `(NASA); Mission`
    

    You would instead have to put the newline before the closing paren, which is pretty gross.

As I said in the proposal, I also think this syntax is kind of arbitrary—it doesn’t really resemble any similar syntax in another language or any other use of parentheses in Swift. If anything, it might look like a C-style cast, which would be pretty misleading.

6 Likes

It doesn’t feel arbitrary (to me) as parentheses are used in English for this purpose. One could say that :: is an arbitrary marker as well.

I would expect it to work too, but the shortcuts available for initializers and callAsFunction aren't "separate custom" that require any further invention than already got invented. The function names are extra noise on top of the required noise.

S.MyModule::init(label: argument)
S.MyModule::(label: argument)
S().MyModule::callAsFunction(label: argument)
S().MyModule::(label: argument)

A requirement for either of those suggests the spelling on top here, rather than on the bottom, which is what key paths look like.

S().MyModule::subscript(label: argument)
S().MyModule::[label: argument]

My gut feeling is that if your code base is structured in such a way that you're frequently needing to use module selectors to disambiguate cases like this, you have deeper problems than just having to type a few more characters to specify the identifier.

4 Likes

I’ve spent all morning reviewing it and considering conversations I’ve had about it.

I’m very much in favor of this feature! +1

Import Grammar and Ergonomics

The proposal introduces grammar for a new import-declaration which supports the module-selector. That enables forms like:


// note: There don't appear to be examples of this in the proposal. Please let me know if this is not correct

import class Module::ModuleClass

This might be somewhat off topic, but I was wondering if it'd be feasible to instead do this:


import Module::ModuleClass

If technically feasible, not including the import-kind requirement on this import-declaration would be a nice quality-of-life improvement - easier to remember, more compatible with autocomplete, and easier to parse when reading (at least for me).

Motivation

The motivation section could do a better job illustrating more directly how these issues manifest for developers in the Swift ecosystem. Right now only “Unqualified lookups are prone to shadowing” has examples; the other problem areas are strictly prose and would benefit from illustrative code snippets when possible. And ensure those broken examples get re-iterated in the proposed solution section.

I think a key point worth stressing is that ambiguity often arises from choices made by others, leaving someone stuck searching for workarounds, whether that's the app developer or the compiler.


Excellent work!

3 Likes

You're right!

But adding this feature is not about encouraging creating these scenarios, it's about solving them when they happen to arise, especially the ones that require arcane or excessively complex workarounds.

Anecdotally, the most recent collision I encountered was when a pretty old package (Siesta) which existed before SwiftUI did was imported in the same file as SwiftUI and they both have the type Text. This example is resolvable without the new feature, but is illustrative of the sorts of situations that might lead a developer to face one of the unresolvable examples.

1 Like

Of course, but solving them—which this proposal does—is different than optimizing for them at the cost of other implementation complexity like allowing the identifier portion of a module selector to be empty and having to plumb it through the same type checker logic that has to decide whether S(...) is a function call or an initializer.

1 Like

I think it would be fair to ask if the proposal could address whether the change has a material impact on compiler performance!

However, the proposal attempts to justify the features "cost" in its motivation section. If you don't think the motivation justifies accepting it because those motivations don't justify the "cost", could you elaborate?

Independent of the source language change, of the most important benefits from this proposal is that module interfaces become more robust. Today, if your module defines a type with the same name as the module, you cannot build for distribution, which is a pretty severe limitation.

8 Likes

I still find it odd for the module selector to be infixed. The spelling NASA.Spacecraft::Engine reads as referring to an Engine type in the NASA.Spacecraft module. And yes, Swift has submodules—it’s just that the only syntax for declaring them right now is a Clang module map. If you do a grep for explicit module in the macOS 26 SDK, you get over 200 hits for things like os.activity, Security.CodeSigning IOKit.audio, and UIKit.UIGestureRecognizerSubclass.

I request the proposal author and the LSG explicitly acknowledge this truth while evaluating this proposal. Once we have all agreed on this reality, there is an immediately obvious problem with the proposed syntax: you can’t lexically parse resolve A.B::C without doing a lookup to determine whether A.B is a fully qualified submodule in the current lookup scope. It could either refer to a declaration of C in the submodule A.B, or the nested type A.C declared in the module B. What if both are possible?

I am absolutely sold on the need for a solution to this problem, but I believe this proposal is fatally flawed. I have described an alternative in the pitch thread linked above.

Edit: It’s not a lexical problem; that would mean a problem with the updated language grammar. It‘s a problem for symbol resolution and for humans reading the code. I regret the error.

5 Likes

Thank you for that context! I think that'd be something great to add to the proposal! The interface files are important and being able to reliably generate ones that are correct (robustness) seems like a table stakes motivation. It almost doesn't need a specific example if framed that way, but the section actually downplays the importance by explaining all the reasons we've been getting by. But it sounds like this might be a primary motivation, paying down technical debt, and completing a feature of the language that has essentially (more or less) been broken for a long time. Folks like to see investment in completing things in the language

1 Like

Until version 5, Alamofire's docs included examples that used Alamofire.request to demonstrate usage. While this looked like a module reference (Mattt was probably anticipating being able to reference module APIs without explicit import), it was in fact a public global vended by Alamofire, essentially a duplicate request interface for all of the top level API. If you used import Alamofire, you saw all of these global functions. During the development of version 5 I removed those globals, both to remove duplicate APIs and to remove them from the global namespace. My first attempt was enum Alamofire, but that ran into the obvious conflict with the module name. So I had to settle on AF (AF.request(...), etc.).

With this proposal adding disambiguation syntax, I could've added the enum Alamofire namespace, but it would be nice if people didn't have to use Alamofire::Alamofire.request to get equivalent API again. (I probably still would've renamed it if that was the case.) Would this proposal enable defaulting to a module's vended names, rather than the module itself? If the user needs to disambiguate they can.

1 Like

Okay, let's talk about the extent to which submodules do and don't exist in Swift.

Clang submodules are real and importing the right submodule is necessary to make a declaration visible to Swift. However, other than their effect on visibility, Swift flattens away submodule distinctions during name lookup. Declarations from a submodule behave as though they were declared in the top level module. If you try to name the submodule when you look up a declaration, the lookup fails.

Here's Swift 6.2 demonstrating this behavior:

# When submodule is imported, type is accessible through top-level module
$ swift -e 'import os.activity' -e 'print(os.os_activity_flag_t.self)'
os_activity_flag_t

# When submodule is not imported, type is not accessible
$ swift -e 'import os' -e 'print(os.os_activity_flag_t.self)'
-e:2:7: error: module 'os' has no member named 'os_activity_flag_t'
1 | #sourceLocation(file: "-e", line: 1)
2 | import os
3 | print(os.os_activity_flag_t.self)
  |       `- error: module 'os' has no member named 'os_activity_flag_t'
4 | 

# Submodule cannot be named when referencing the type
$ swift -e 'import os.activity' -e 'print(os.activity.os_activity_flag_t.self)'
-e:2:7: error: module 'os' has no member named 'activity'
1 | #sourceLocation(file: "-e", line: 1)
2 | import os.activity
3 | print(os.activity.os_activity_flag_t.self)
  |          `- error: module 'os' has no member named 'activity'

Note that third example: Even though you write os.activity in the import statement, you cannot actually reference the type as os.activity.os_activity_flag_t when you use it. You must call it os.os_activity_flag_t (or leave off the module name, of course).

That is the current reality of submodules. It is how the compiler has behaved for eleven years, and it is how the language was designed to behave.


The module selectors proposal does not alter this existing design; it just applies it to the new feature. Submodules are still used in import statements to control visibility, but they are not used when referencing a declaration in source code. A module selector for the top-level module will find visible declarations in submodules, just like a module-qualified lookup would.

# When submodule is imported, type is accessible through top-level module
$ ../build/<snip>/swift -e 'import os.activity' -e 'print(os::os_activity_flag_t.self)' -enable-experimental-feature ModuleSelector
os_activity_flag_t

# When submodule is not imported, type is not accessible
$ ../build/<snip>/swift -e 'import os' -e 'print(os::os_activity_flag_t.self)' -enable-experimental-feature ModuleSelector
-e:2:7: error: cannot find 'os::os_activity_flag_t' in scope
1 | #sourceLocation(file: "-e", line: 1)
2 | import os
3 | print(os::os_activity_flag_t.self)
  |       `- error: cannot find 'os::os_activity_flag_t' in scope

# Submodule cannot be named when referencing the type
$ ../build/<snip>/swift -e 'import os.activity' -e 'print(os::activity::os_activity_flag_t.self)' -enable-experimental-feature ModuleSelector
-e:2:11: error: module selector cannot specify a submodule
1 | #sourceLocation(file: "-e", line: 1)
2 | import os.activity
3 | print(os::activity::os_activity_flag_t.self)
  |           `- error: module selector cannot specify a submodule

# Not with a dot between `os` and `activity`, either
$ ../build/<snip>/swift -e 'import os' -e 'print(os.activity::os_activity_flag_t.self)' -enable-experimental-feature ModuleSelector
-e:2:7: error: module 'os' has no member named 'activity::os_activity_flag_t'
1 | #sourceLocation(file: "-e", line: 1)
2 | import os
3 | print(os.activity::os_activity_flag_t.self)
  |       `- error: module 'os' has no member named 'activity::os_activity_flag_t'

Could module selectors be designed differently? Could we make it so that os::os_activity_flag_t does not work and you must write os::activity::os_activity_flag_t instead? Probably. But this would be a change from how submodules interact with other name lookup features, and nobody has made a convincing case yet for why it would be a good idea. In the absence of a motivation to treat submodules differently, the default design is to treat them the same way.


In theory, it might also be possible to reopen the question of the overall submodule design, such that os.activity.os_activity_flag_t would be supported too. However, such a change is beyond the scope of this proposal, which introduces an additive feature that works within the existing design. If we wanted to change the basic way submodules work in Swift, that change would deserve its own proposal and review focusing on its details.

(In practice, changing the overall submodule design like this would be source-breaking and is probably not practical.)


Because references to declarations never name a submodule, this statement is not correct:

We know that B is not a submodule of A because you are neither required nor permitted to name a submodule in a module selector. It therefore must be the case that A is a type without a module selector and B is the module selector on C.

If we did want to support module selectors, however, we could still make it unambiguous by requiring developers to separate a module selector's module and submodule with a double colon instead of a dot:

A.B::C     // unambiguously references (`C` from module `B`) inside `A`
A::B::C    // unambiguously references (`C` from module `A.B`)

The proposal mentions that the latter syntax is intentionally invalid. This is so that, if a need arises in the future to support submodules in module selectors, that feature can be added without risking source breaks.

10 Likes

The proposal does not change the fact that type names shadow module names in Swift's name resolution algorithm for existing syntax. If there were an public enum AlamoFire in the AlamoFire module, AlamoFire.request written in client source code would refer to the request member of that enum, both before and after this proposal. What this proposal does is make it possible to use a different syntax to get around those existing shadowing rules when you really need to refer to AlamoFire the module instead of the enum.

2 Likes

Six years ago, my experience was the opposite. Referring to Alamofire.request when it was moved in the enum, resulted in "Alamofire has no member request" errors. But perhaps I'm misremembering.

I believe you're correct, the specific message is Module 'Alamofire' has no member named 'request', leaving use we no syntax to refer to struct Alamofire

The only way I can reproduce what you folks are describing is if I use a scoped import to bring specific top-level declarations into scope:

import struct AlamoFire.SomethingElse

func test() {
  AlamoFire.request() // error: Module 'AlamoFire' has no member named 'request'
}

This is scoped imports working as intended, though. The user has explicitly stated that they only want struct SomethingElse brought into scope, so enum AlamoFire is not in scope and doesn't shadow the module.

If you have a different example where this reproduces with just import AlamoFire I'd be curious to see it, but I think any example would be demonstrating a compiler bug rather than an intended behavior of the language.

The way I think about init and callAsFunction is that, once you've introduced the member lookup dot, you need to provide a complete member name, not a fragment of one. That is, you can't write S.MyModule::(label: argument) because you can't write S.(label: argument).

Following this idea to its logical conclusion probably means that if we allowed you to write a module selector on a subscript, it'd be spelled s.MyModule::subscript(label: argument), but I don't love calling subscripts with parentheses. Leaving subscripts as a future direction is a (somewhat cowardly, tbh) way to avoid the question.

2 Likes