SE-0444: Member import visibility

Hello Swift community,

The review of "Member import visibility" begins now and runs through September 23, 2024. The proposal is available here:

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email. When emailing the review manager directly, please include "SE-0444" in the subject line.

Trying it out

If you'd like to try this proposal out, you can download a toolchain supporting it here . Any trunk development snapshot dated September 6, 2024 or later should do (newer snapshots may include diagnostic improvements). You will need to add -enable-experimental-feature MemberImportVisibility to your build flags.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

swift-evolution/process.md at main · apple/swift-evolution · GitHub

Happy reviewing,

—Becca Royal-Gordon
SE-0444 Review Manager

28 Likes

+1

While I haven't hit this particular confusing scenario often, I have stumbled through it a couple of times.

I think the change is useful enough to be accepted, especially with the fixit to help guide those cases where you might have been relying on a transitive import that would no longer fully compile.

I've not used this specific toolchain and tried out the proposed feature, only read through the proposal.

Resounding +1.

We've invested in building tools that help users manage their imports based on what they use in their sources (Ă  la include-what-you-use, but for Swift), and the leakiness of extensions has caused no end of special cases that we have to consider.

The proposed lookup rules are predictable and make sense. They will require users to write more imports than they otherwise have to today (and thus will require some migration when the feature is enabled), but that's a feature and not a bug: developers should always be aware of what they're pulling into their compilation units instead of accidentally relying on spooky-imports-at-a-distance.

15 Likes

+1

The current "leaky" behavior is confusing and should be changed. Additionally, doing so will finally open the door for tooling / compiler features that are able to detect unused dependencies in Swift, which is something that historically couldn't be done because of the issue that this proposal addresses.

5 Likes

Absolute +1.

I've encountered this many times and adjusted my entire coding style to avoid this issue. Thinking of the legacy projects I've worked on, it'll also help know if a dependency is actually used by just searching import <Dependency> without getting as many false negatives!

I have a background question. Let’s say framework Foo imports framework Bar. What controls whether Foo.framework emits an “autolink Bar” symbol—does this only happen if some source file in Foo contains @exported import Foo? And does the compiler require an @exported import in the same source file that uses any of Bar’s types as part of Foo’s public API? Does that depend on language mode?

LGTM! Would there be any edge-casey stuff to defend against once we enable the changes from SE-0409 as default (which I believe was originally planned for 6.0 but then pushed back to 6.1+)?

SE-0409 encourages library authors to reduce the transitive dependencies they expose to clients of their libraries (by requiring that exposed imports be explicitly marked public). This proposal, on the other hand, requires in more cases that clients explicitly import the dependencies that they use. Both proposals are tackling the general problem of predictable dependencies in Swift but from different angles.

I think the interaction between the proposals is generally positive, since clients directly importing the dependencies they need means that the act of removing some public dependency from a library will not be as likely to be source breaking for the clients. I haven't been able to come up with any more interesting interactions that feel relevant.

3 Likes

All imported framework modules that are not @_implementationOnly or internal or package or @_spiOnly, direct and transitive, result in the compiler inserting an autolink load command, without a need for @exported. And this does not depend on the language mode.

One thing worth noting though is that, on Darwin, if the linker is unable to resolve to autolink load command, that alone will not constitute a link failure if all symbols are otherwise successfully resolved.

3 Likes

Strong +1

This proposal will go a long way towards avoiding the introduction of unexpected module dependencies in large code bases, and the resulting name lookup rules are far easier for users to reason about. The proposed diagnostics for name lookup failures should also allow for relatively easy incremental adoption.

Will static members like typealiases be subject to the same rules? For example:

import SwiftUI

extension View {

     typealias Text = CursedText 

}

Strong +1 This is really how imports should have worked from the beginning.

Yes, the proposal would affect lookup of member declarations in general. The new behavior applies equally to static and instance members. It also applies equally to all kinds of member declarations (functions, types, typealiases, etc.).

2 Likes

+1. Really looking forward to this feature. If this is approved and landed are we aware of any other outstanding hole where types/members can be used without having an appropriate import or is this the last hole that needs closing?

Yes. Retroactive conformances and operator declarations (also precedence groups?), at a minimum.

On that note, I was under the impression that this proposal would cover operator function implementations declared as static members, but the text does not spell that out and seems to hedge on what the behavior actually will be.

IMO, this proposal needs to either spell out that it doesn’t change the status quo in that respect, or that it does (in which case, it should work just as any other static member does), but it shouldn’t leave the effect of this proposal on a subset of static members just unspecified.

4 Likes

I see. Do we have GitHub issues tracking those gaps?

Even in a language mode that has this proposal there are still a number of ways for a source file to have module dependencies that are unacknowledged by import statements. This is not an exhaustive list, but here are a few examples of how it can happen:

Conformances

A conformance may be used without importing the module that defines the conformance:

// 'Far' module

// Do NOT copy this retroactive conformance into library code!
extension String: @retroactive Error {}
// 'Near' module
import Far
// Client
import Near

func f() throws {
  // Uses the conformance from 'Far'
  throw "Implicit use of Far's conformance of String to Error"
}

Conformances aren't subject to name lookup rules since you don't "name" a conformance at its point of use in Swift. The proposal mentions this in Future directions, specifically with respect to retroactive conformances since they create the potential for ambiguity (when the compiler can see multiple conformance declarations it's not defined how it should pick the one to use). However, it's also worth clarifying that even non-retroactive conformances can be used in a source file without explicitly importing the module that defines the conformance.

Clang re-exports

As mentioned in the proposal, it is conventional for Clang modules to use export *, which has the effect of re-exporting every module that is imported by the Clang module. As a concrete example of how this can result in surprising dependencies, take this program which may be compiled successfully against Apple's SDKs:

import SwiftUI

let _ = AttributedString("...")

AttributedString comes from Foundation, which isn't imported in this file. SwiftUI also does not directly re-export Foundation. However, it does re-export a couple of other modules, which happen to transitively export some Clang modules from the SDK. Those Clang modules ultimately import Foundation and bunch of other Clang modules, and since most of the Clang modules have export *, the set of modules visible to name lookup becomes quite large (I counted a total of 38 visible modules from a single import SwiftUI when testing this using the macOS SDK shipped with Xcode 15.4).

Strictly speaking, this is all expected behavior according to the rules of re-exports, but it doesn't really feel that intentionally designed to me.

Compiler inserted calls to value witnesses

Consider the following example, where a client program uses the APIs of an imported module to work with types from a transitively imported module:

// 'Far' module
public struct Foo {}
// 'Near' module
import Far

public func returnsFoo() -> Foo
public func consumesFoo(_ x: consuming Foo)
// Client
import Near

let x = returnsFoo()
consumesFoo(x) // semantically copies 'x'
consumesFoo(x)

Since type inference allows us to avoid naming Foo anywhere in the client source file, the compiler does not require the Far module to be imported. However, the code generated for the program will contain references to symbols from the Far library. In this specific example, the copy value witness for Foo would be implicitly called to satisfy the lifetime requirements for the value x.

3 Likes

+1
although I haven't encountered this very often (which in a sense actually makes it worse) it really trips you up. Being explicit is the better solution.

Does it fit well with Swift?
I believe it does. Even if its a code breaking change, I choose being explicit over something that "typically works, until it doesnt".

1 Like

+1 but if this is the first time Swift is borrowing namespace syntax from C++, why not borrow from its key path syntax instead: "Hello"\RecipeKit.parse(). The backslash in Swift could become synonym for identifying something within the scope of something else (in this case, a Module). And in places where the backslash is already used, you’d be able to say \Module.Type.element, if that makes sense.

Operator lookup is implemented using a distinct algorithm, separate from member lookup and top-level name lookup. My tests for the implementation of MemberImportVisibility indicate that the leaky visibility problem doesn't apply to operators declared as members. An operator declared as a member in a transitively imported module doesn't appear to be considered a candidate. I can amend the proposal to clarify that it doesn't apply to operators.