SE-0529: Add FilePath to the Standard Library

Hi all, I thought I'd post a TL;DR; from this discussion thread to sync up on what has changed and what hasn't:

Changes

We're arguing for the following API changes:

  • FilePath.init(_ string: String) becomes failable, matching the corresponding inits on Anchor and Component. The Span-based init becomes failable as well. Both reject input containing NUL. init(stringLiteral:) keeps its trapping behavior for ill-formed literals.
  • resolve() is annotated @available(*, noasync).
  • nullTerminatedCodeUnits is removed and replaced by a closure-based withCString(_:) tracking String.withCString(_:). The body's pointer parameter is UnsafePointer<FilePath.CodeUnit>, so it is wide on Windows. String already establishes via withCString(encodedAs:_:) that this name is not narrow-only.
  • isRelative is removed; isAbsolute remains. There are several distinct kinds of relative on Windows (foo\bar, C:foo\bar, \foo\bar), which a single negated property obscures.

Future work

The proposal's existing Future Directions cover several deferred items already. Based on this thread, we want to clarify or expound on the following:

  • The multi-platform path library direction: failable cross-platform conversions and possibly a generic-over-storage path type, distinct from the currency-type role FilePath is playing here.
  • Async non-blocking and sync non-blocking variants of resolve(), both pending a stdlib I/O pool design.
  • Richer validation API for degenerate path forms. The new failable inits reject only NUL; further validation requires application-specific configuration.
  • A / operator for append, with the component-vs-sequence-vs-join question called out explicitly.
  • Two language-level wishes that would shape future FilePath API:
    • a standard story for null-terminated pointer types (e.g. a var cString instead of closure-based withCString)
    • an effects system for blocking I/O that would let resolve() carry the corresponding annotation.

Kept, with rationale

resolve() stays in the proposal, synchronous and blocking, marked @noasync. Correct path resolution is hard to get right and easy to get wrong: it interacts with symlinks, .. segments, mount points, Darwin volfs anchors, and Windows reparse points. Without a stdlib implementation from day one, developers will write their own incorrect or racy versions. Async non-blocking is a much needed, but longer-term, variant that depends on an I/O pool. Async+blocking would be worse to ship than sync+blocking, since it would silently block executors.

We're keeping resolve() as the proposed name. We'll add a new Alternatives Considered subsection for the alternative names proposed (resolveByBlocking(), resolveFromFileSystem(), sync/async pairs, namespaced free functions) for LSG visibility. We're still recommending the original name but curious how the LSG views this issue.

Character is kept for separator and driveLetter. The intended usage is more print()-like than low-level parser-like. Code that parses path bytes by hand is already platform-encoding-specific enough to zero-extend the separator (U+002F or U+005C) via separator.utf8.first!. Better support for custom parsers is future work.

Cross-platform parsing of string literals is by design. A POSIX literal parses correctly on Windows and renders with \. Some literal forms remain unavoidably platform-specific, and that is a constraint of multi-platform support rather than a defect of FilePath.

FilePath tracks the target platform under cross-compilation, not the host. It is the path type for the running process, intended to be handed to the OS. Use cases wanting host syntax in macros and plugins are better served by the multi-platform path library above.

10 Likes