- Proposal: SE-0285
- Author: Becca Royal-Gordon
- Review Manager: Tom Doron
- Implementation: apple/swift#32700
- Status: Implemented (Swift 5.3)
- Decision Notes: Rationale
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 made the following changes:
-
Introduced a new
#filePath
magic identifier which, like#file
in prior versions of Swift, evaluates to the source file path passed when the compiler was invoked. -
Modified the
#file
magic 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
#filePath
default argument inadvertently captured#file
instead, 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 }
#endif
They 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) }
#endif
While 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.
We propose to modify all three aspects of SE-0274's changes:
-
In addition to
#filePath
, we will introduce a new#fileID
magic identifier, which generates the new concise file string in all language modes. -
#file
will continue to produce the same string as#filePath
in the Swift 5 and earlier language modes. When code is compiled using future language modes (e.g. a hypothetical "Swift 6 mode"[1]),#file
will generate the same string as#fileID
, and#fileID
will be deprecated. -
In Swift 5 mode, for the purposes of the "wrong magic identifier literal" warning,
#file
will be treated as compatible with both#filePath
and#fileID
. In future language modes, it will be treated as compatible with only#fileID
.
[1]
"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.
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.
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
.
Swift will not warn when:
- A parameter with a default argument of
#file
is passed to one with a default argument of#fileID
. - A parameter with a default argument of
#fileID
is 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
#file
is passed to one with a default argument of#filePath
. - A parameter with a default argument of
#filePath
is 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.
This guideline will be added to the "Parameters" section of the Swift API Design Guidelines:
- If your API will run in production, prefer
#fileID
over alternatives.#fileID
saves space and protects developers’ privacy. Use#filePath
in 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#file
to preserve source compatibility with Swift 5.2 or earlier.
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.
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.
See SE-0274.
See SE-0274.
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.