SE-0444: Member import visibility

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