[Accepted with modifications] SE-0409: Access-level modifiers on import declarations

Hi all,

The review of SE-0409: Access-level modifiers on import declarations concluded on September 26th.

The Language Steering group has decided to accept the proposal with a single modification. Namely, in addition to access level modifiers (internal, public, etc.), import declarations will be able to be marked with @usableFromInline to mark an imported module whose declarations should be available for use in the body of @inlinable functions, but still restricted from appearing in the public interface of the importing module. The Language Steering group agreed that it did not make sense to force users to abandon the interface-level checking provided by this proposal (by making the import a public import) just to use a declaration from an inlinable function body.

Overall reception of the feature was generally positive, at least as to the problem raised being worth solving. There were some questions raised as to whether the proposed solution was on the right track.

Some reviewers wondered whether it was appropriate for global property such as "is this module a public dependency or an implementation detail" to be inferred based on the upper bound of it's imported visibility across all source-file-local import declarations, and suggested that marking dependencies with this information in, say, the package manifest might be a better approach. While the Language Steering Group does not rule out the possibility of some sort of marking in the build system which could enforce "this module cannot be imported at higher than internal visibility," the Group felt that:

  • The ultimate effect of different import access levels was significant enough to warrant some sort of marking in the source file, so that users reading code can easily see which dependencies in the file are permitted to be used in the interface with the corresponding access level(s).
  • The problem being solved is sufficiently a language-level concern that it should not be only the responsibility of the build system to support/enforce dependency visibility—it should be a first-class language feature that Swift itself allows programmers to express regardless of the build system they are using.

Accepting that some source-level marker (attached to import declarations) was appropriate, the Language Steering Group considered the concerns about the potential unclarity of the public import Foo spelling. Some reviewers felt that this spelling incorrectly implied behavior more similar to @_exported than what was actually proposed. That is, reviewers expected that public import Foo would 're-export' the contents of the Foo module for use from the importing module's namespace. One reviewer noted that perhaps a more accurate, if more awkward, spelling would be @usableFromPublic, @usableFromInline, etc.

The Language Steering Group acknowledges that the public in public import Foo has a different meaning than the public in, say, public struct Bar. However, the Group elected to stick with the proposed spelling for the following reasons:

  • The use of public import in library code is likely to be relatively pervasive, and a more heavyweight, explicit spelling like @usableFromPublic or @usableFrom(public) or similar would quickly become too noisy.
  • The possible misinterpretation is relatively benign: users who expect public import Foo to re-export Foo's contents from their own module will quickly discover that this is not, in fact, its function, and then be able to correct their usage in whatever way they choose (e.g., by applying @_exported).
  • Even if we were to formalize @_exported as an official language feature, we would not want to use the public import Foo spelling for such a specialized operation with such wide-reaching implications, and so using the public import spelling for the behavior proposed here does not 'close off' its use for different functionality in a way that is concerning.

There were some further questions and concerns raised about details of the proposal:

  • Some felt that the inclusion of the private access level for import statements was unnecessarily fine-grained. The Language Steering group felt that the use case of a wrapper file around a library that an author does not want to accidentally 'leak' to the rest of the module was sufficiently justification for private import, and that the alternative of factoring that file out into a separate module was too burdensome to be a suitable solution.
  • Others noted that private and fileprivate were redundant in this proposal because import declarations can only appear at file scope, and suggested that the language should just pick one to be used with import declarations and ban the other. The Language Steering group does not feel that an arbitrary restriction here is warranted, and that import declarations should be able to be marked as either private or fileprivate just like any other top-level declaration.

Thank you for participation in the evolution process!

Frederick Kellison-Linn
Review Manager

31 Likes

Have I understood correctly that internal, package or public imports will not allow for usage of the imported module’s symbols from other files within the same module as the file with the import statement? I currently use @_exported import in a single, dedicated file in each module to avoid having to rewrite the import declarations in each file of the module. Is this considered an anti-pattern? Will @_exported internal import Foo provide the symbols of Foo to all files in the module without exposing Foo to clients of the module?

Hello Swift community,

As the language has made additional progress towards the Swift 6 milestone, the Language Steering Group has been evaluating what the migration to the Swift 6 language mode is expected to look like for a variety of projects. As a result of this evaluation, the the Language Steering Group has decided that the upcoming feature flag InternalImportsByDefault declared by SE-0409 will no longer be enabled by default in Swift 6 mode.

The practical consequences of this decision are that in Swift 6 mode the default access level for import statements will remain public, just as in Swift 5. The InternalImportsByDefault flag will be enabled in a future, post-Swift-6 language mode. Explicit use of access control keywords on import statements, such as internal import , will be fully supported in Swift 6, and users who wish to proactively make use of the feature will of course still be able to do so: this is only a deferral of the source-breaking aspects of the proposal. Projects which explicitly write public import for all publicly-used imports will have an easier migration path for the future language version which enables InternalImportsByDefault .

The Language Steering Group's decision was based on a few factors:

  • With the realities of the the Swift 6 migration taking place, one emergent piece of information has been that in many cases, a substantial amount of the changes required come from changing the default access level of import statements as described in SE-0409. Moreover, since there has not been (and will not be) a Swift release which supports the changes in SE-0409 before the release of Swift 6.0, authors of library code (the primary demographic affected by this source break) who wish to support the Swift 6 language mode while also continuing to support compilation by older compilers would need to do a complex #if dance around a large number of import statements.
  • As announced previously, the Language Steering Group has narrowed the focus of the Swift 6 language mode to enabling data-race safety by default. While this proposal (and the associated source break) were accepted before that announcement, the Group believes that had the timing on this proposal been marginally different, i.e., had it been reviewed after the focus of Swift 6 had been narrowed, the source break proposed by SE-0409 would not have been accepted for Swift 6.
  • More implementation work is needed for the compiler to be able to take advantage of optimizations available for modules which are imported at most internally throughout an entire client module, so the benefit of flipping the switch now is relatively small compared to the large amount of source churn it would cause.

As always, thank you to everyone for your feedback and for helping make Swift a better language.

25 Likes

This apparent (?) typo (InternaImports instead of InternalImports - missing lowercase L) appears several times in the above and is potentially mildly confusing.

Cheers.

1 Like

Thank you, corrected!

1 Like