SE-0285: Ease the transition to concise magic file strings
* Proposal: [SE-0285](0285-ease-pound-file-transition.md) * Author: [Becca Royal-Gordon](https://github.com/beccadax) * Review Manager: [Tom Doron](https://github.com/tomerd) * Implementation: [apple/swift#32700](https://github.com/apple/swift/pull/32700) * Status: **Implemented (Swift 5.3)** * Decision Notes: [Rationale](https://forums.swift.org/t/accepted-se-0285-ease-the-transition-to-concise-magic-file-strings/38516)
Introduction
In [SE-0274], the core team accepted a proposal to change the behavior of #file. This proposal modifies that plan to transition into new behavior more gradually, treating it as a source break requiring a new language version mode to fully adopt.
Swift-evolution thread: Revisiting the source compatibility impact of SE-0274: Concise magic file names
[SE-0274]: https://github.com/swiftlang/swift-evolution/blob/master/proposals/0274-magic-file.md
Motivation
SE-0274 made the following changes:
- Introduced a new
#filePathmagic identifier which, like#filein prior versions of Swift, evaluates to the source file path passed when the compiler was invoked.
- Modified the
#filemagic identifier to instead evaluate to a string of the formModuleName/FileName.swift. (See SE-0274 for discussions of the motivations for this.)
- Added a warning when, essentially, a wrapper around a function with a
#filePathdefault argument inadvertently captured#fileinstead, or vice versa.
After acceptance, changes 1 and 3 were enabled in the release/5.3 branch. Change 2 was gated by a staging flag and was expected to be enabled in the release after 5.3.
On paper, this seemed like a workable plan, giving developers the entire Swift 5.3 cycle to update code like this:
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #file) { print(file) }To this when necessary:
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #filePath) { print(file) }However, many source-distributed libraries like SwiftNIO need more than one version of backwards compatibility in use sites which will need to transition to #filePath. Their authors had believed they could achieve this using #if compiler(>=5.3) directives, but when they began to implement these workarounds, they found they were stymied by language limitations.
For instance, they believed they would be able to use a helper function to abstract the choice of #file or #filePath, not realizing that the magic call-site behavior of #file and #filePath in default arguments isn't transitive:
/// Prints the path to the file it was called from.
/// - Warning: Actually prints the path to the file `printSourcePath(file:)` was
/// defined in instead!
func printSourcePath(file: String = filePathOrFile()) { print(file) }
#if compiler(>=5.3)
func filePathOrFile(_ value: String = #filePath) -> String { value }
#else
func filePathOrFile(_ value: String = #file) -> String { value }
#endifThey also believed they could use #if in default arguments, not realizing that #if directives can only be wrapped around whole statements or declarations:
/// Prints the path to the file it was called from.
/// - Warning: Actually fails to compile with a syntax error!
func printSourcePath(file: String = #if compiler(>=5.3)
#filePath
#else
#file
#endif) { print(file) }That left them with with only one reasonable option—conditionally-selected wrapper functions—which they understandably considered too burdensome:
fileprivate func _printSourcePathImpl(file: String) { print(file) }
#if compiler(>=5.3)
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #filePath) { _printSourcePathImpl(file: file) }
#else
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #file) { _printSourcePathImpl(file: file) }
#endifWhile we still want to reach the same endpoint—most code using a syntax with concise file strings while full paths are still available to the use sites that need them—it has become clear that this transition needs to be more gradual, with #file continuing to have its current meaning in Swift 5 projects indefinitely.
Proposed solution
We propose to modify all three aspects of SE-0274's changes:
- In addition to
#filePath, we will introduce a new#fileIDmagic identifier, which generates the new concise file string in all language modes.
#filewill continue to produce the same string as#filePathin the Swift 5 and earlier language modes. When code is compiled using future language modes (e.g. a hypothetical "Swift 6 mode"[1]),#filewill generate the same string as#fileID, and#fileIDwill be deprecated.
- In Swift 5 mode, for the purposes of the "wrong magic identifier literal" warning,
#filewill be treated as compatible with both#filePathand#fileID. In future language modes, it will be treated as compatible with only#fileID.
<details>
<summary>[1]</summary>
"Swift 6" is purely illustrative. There is no guarantee that Swift 6 will be a source-breaking version, that there won't be a source-breaking version before Swift 6, or indeed that there will ever be a Swift 6 at all. Offer void where prohibited.
</details>
Detailed design
The #fileID magic identifier literal
In addition to the existing #file and the new #filePath, Swift will support #fileID. This new magic identifier will generate the same module-and-filename string SE-0274 specified for #file. It will do so immediately, without any way to reverse its behavior.
Standard library assertion functions like assert, precondition, and fatalError will switch from #file to #fileID. When a filename is included in a compiler-generated trap, such as a force unwrap, it will also emit a literal equivalent to using #fileID.
#fileID is intended to allow Swift 5.3 code to adopt the new, more compact literals before the behavior of #file changes. In language version modes where #file produces the same string as #fileID, #fileID will be deprecated.
Transitioning the #file magic identifier
The #file identifier will continue to generate the same string as #filePath in Swift 4, 4.2, and 5 modes. In any future language version modes, it will instead generate the #fileID string. This means that the change in #file's behavior—and therefore the requirement to change some uses of #file to #filePath—will be delayed until Swift next makes source-breaking changes, and even then, the current behavior will be preserved in Swift 5 mode.
If a client compiled in Swift 5 mode or earlier uses a library compiled with a later language mode, the library's #file default arguments will be treated as though they were #fileID; if a client compiled with a later language mode uses a library compiled in Swift 5 mode or earlier, the library's #file default arguments will be treated as though they were #filePath.
Magic identifier default argument mismatch warnings
Swift will not warn when:
- A parameter with a default argument of
#fileis passed to one with a default argument of#fileID. - A parameter with a default argument of
#fileIDis passed to one with a default argument of#file.
Additionally, Swift 5 mode and earlier will not warn when:
- A parameter with a default argument of
#fileis passed to one with a default argument of#filePath. - A parameter with a default argument of
#filePathis passed to one with a default argument of#file.
In Swift 5 mode, substituting #file for #fileID will actually result in different behavior, so ideally we would warn about this. However, doing that would cause new warnings in existing functions which wrap functions that adopt #fileID. In practice, most #fileID adopters will work fine when they're passed #file—they'll just generate unnecessarily large file strings.
Swift API Design Guidelines amendment
This guideline will be added to [the "Parameters" section][api-params] of the Swift API Design Guidelines:
- If your API will run in production, prefer
#fileIDover alternatives.
#fileIDsaves space and protects developers’ privacy. Use#filePathin APIs that are never run by end users (such as test helpers and scripts) if the full path will simplify development workflows or be used for file I/O. Use#fileto preserve source compatibility with Swift 5.2 or earlier.
[api-params]: https://swift.org/documentation/api-design-guidelines/#parameter-names
Schedule
Ideally, these changes will be included in Swift 5.3, with the "future language mode" parts tied to a frontend flag for testing, rather than a language version mode.
Source compatibility
Significantly improved compared to SE-0274. Code that uses #filePath or #fileID will not be compilable in Swift 5.2 and earlier, but such code can continue to use #file until a future language version mode permits breaking changes.
Effect on ABI stability
See SE-0274.
Effect on API resilience
See SE-0274.
Alternatives considered
The SE-0274 design was considered and accepted, but did not hold up to real-world use.
In future language version modes, we could deprecate #file instead of #fileID, creating a situation where users must choose whether to update their uses of #file to either #fileID or #filePath. This would prompt future users to make a choice of behavior, but we don't want to force those users to make an explicit choice; we continue to believe that the concise file string (i.e. #fileID string) is the right choice for most uses.
Similarly, we could simply introduce #fileID and change the standard library to use it, without adding #filePath or changing #file's behavior. But since we think the #fileID string is the right default, this would leave us with a design that encouraged the wrong defaults.
We could remove the #fileID syntax from this proposal, probably retaining it in underscored form as an implementation detail used to transition the standard library assertions to the new strings in Swift 5 mode. This would give us a cleaner "Swift 6 mode" without a deprecated #fileID hanging around, but we don't want to keep potential adopters outside the standard library from gaining the advantages of the new strings if they want them.
There are many colors other than #fileID that we could use to paint this bikeshed; we like this spelling because it's shorter than #filePath and suitably vague about the exact format of the string it produces.