RFC: Std.Path
Nobody has claimed this yet.
- Dominant language
- Lean
- Stars
- 9.2k
- Forks
- 990
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 175
Description
Summary
This proposal replaces System.FilePath with Std.Path. FilePath is a plain String wrapper that relies on System.Platform functions to check the OS and handle platform differences such as path separators. Std.Path instead stores a parsed Array Path.Component, that makes all platform specific logic to IO so that path manipulation is pure by default. This proposal is inspired by Python's pathlib and Rust's pathlib crate.
Migration
Std.Path is a prerequisite for Std.FS (#13638) and will be implemented first. Several modules in Init/System/, like FilePath.lean, Uri.lean, and IO.lean, depend on path representation and will need to move to Std/ as part of this migration (Init/System/IO.lean itself is not changed, only the path-related surface area is relocated). The biggest impact is the removal of Coe String FilePath, which forces many files to be refactored to use functions that now require IO like Path.fromString; most of these changes affect Lake, requiring modifications to more than 60 files. Expressions like "src" / "Main.lean" will need an alternative for usability, either a macro or Path.ofPosixString "src/Main.lean" |>.get!. To keep backward compatibility during the transition, Init.System.FilePath and related files like Uri and IO.lean will be kept until the migration is complete, at which point they are deprecated and eventually removed. This full deprecation and removal is a long-term plan, intended for when Std.Path is mature and battle-tested.
| Old | New | Notes |
|---|---|---|
System.FilePath |
Std.Path |
Structural replacement: FilePath is a String wrapper, Path stores Array Path.Component |
FilePath.pathSeparator |
Path.pathSeparator |
IO Char |
FilePath.pathSeparators |
Path.pathSeparators |
IO (List Char) |
FilePath.components |
Path.components |
Was List String; now Array Path.Component |
mkFilePath |
— | No direct equivalent; use Path.ofPosixString or Path.fromString |
Coe String FilePath |
— | Intentionally dropped. |
Path
inductive Path.Component where
| drivePrefix (value : String) -- Windows drive letter prefix, e.g. 'C' (from "C:")
| root (s : String) -- leading separator (/ on POSIX, \ on Windows)
| current -- "."
| parent -- ".."
| normal (value : String) -- ordinary path segment
deriving Inhabited, BEq, Hashable, Repr
structure Path where
private mk ::
components : Array Path.Component
deriving Inhabited, BEq, Hashable, Repr
API
Pure Operations
All of these work directly on the components array. No platform branching, no string parsing. But functions that accept string segments allow strings like "src/Main.lean" as a single segment. It is not split into multiple components, which means it has different meanings on different platforms (one single valid segment on Windows, two segments on POSIX).
| Name | Signature | Description |
|---|---|---|
Path.empty |
Path |
The empty path (no components); joining with any path yields that path unchanged |
Path.isEmpty |
Path → Bool |
True if the path has no components |
Path.isAbsolute |
Path → Bool |
True if path was parsed as absolute |
Path.isRelative |
Path → Bool |
Negation of isAbsolute |
Path.isRoot |
Path → Bool |
True if the path is absolute with no further components (e.g. "/", "C:\\") |
Path.join / / |
Path → Path → Path |
Append components; if right side is absolute it replaces left |
Path.drive? |
Path → Option String |
The drive letter prefix as a string (e.g. "C:"); none on POSIX or relative Windows paths without a drive |
Path.root? |
Path → Option String |
The root separator string ("/" or "\\") if the path is absolute; none for relative paths |
Path.anchor |
Path → String |
Drive concatenated with root (e.g. "C:\\", "/", or "" for relative paths) |
Path.depth |
Path → Nat |
Number of normal components; root, drive, ., and .. are not counted |
Path.normalize |
Path → Path |
Resolve . and eliminate .. components; pure (no symlink resolution); .. above a root is silently dropped |
Path.parent |
Path → Option Path |
Drop the last component; none for root or empty; relative single-segment returns "." |
Path.parents |
Path → Iter Path |
All ancestors from immediate parent up to root, in order |
Path.fileName |
Path → Option String |
Last normal component; none for root, ., or .. |
Path.fileStem |
Path → Option String |
Filename without the last extension |
Path.filePrefix |
Path → Option String |
Filename before the first extension (e.g. "foo" from "foo.tar.gz"); leading dot preserved |
Path.extension |
Path → Option String |
Last extension without the leading . |
Path.hasExtension |
Path → Bool |
True if the filename has at least one extension |
Path.suffixes |
Path → Array String |
All extensions in order, without leading . |
Path.setFileName |
Path → String → Path |
Replace the last component; no validity check (use withFileName for the validated version) |
Path.withFileName |
Path → (fname : String) → (_ : ValidFileName fname) → Path |
Replace the last component; proof auto-solved for string literals |
Path.withExtension |
Path → (ext : String) → (_ : ValidExtension ext) → Path |
Replace the last extension; proof auto-solved for string literals |
Path.addExtension |
Path → (ext : String) → (_ : ValidExtension ext) → Path |
Append an extension without removing existing ones |
Path.withStem |
Path → (stem : String) → (_ : ValidFileName stem) → Path |
Replace the stem, keeping all existing extensions |
Path.startsWith |
Path → Path → Bool |
True if path has the given prefix (component-wise) |
Path.endsWith |
Path → Path → Bool |
True if path ends with the given relative path or filename (component-wise) |
Path.dropPrefix? |
Path → Path → Option Path |
Remove a prefix; none if not present |
Path.relativeTo? |
(base target : Path) → Option Path |
Compute relative path from base to target; none if roots differ |
Path.matchGlob |
Path → String → Bool |
Test the path against a glob pattern (supports *, **, ?, [abc]) |
Path.ofPosixString |
String → Option Path |
Parse a /-separated string; none for empty input; pure |
Path.ofWindowsString |
String → Option Path |
Parse a \-separated string with optional drive prefix; pure |
Path.toPosixString |
Path → String |
Render as a POSIX string (/-separated); pure |
Path.toWindowsString |
Path → String |
Render as a Windows string (\-separated, drive prefix if present); pure |
IO Operations
Path.fromString and Path.toString are IO because detecting the platform separator requires reading runtime state.
| Name | Signature | Description |
|---|---|---|
Path.fromString |
String → IO Path |
Parse using the platform separator; delegates to ofPosixString or ofWindowsString |
Path.toString |
Path → IO String |
Render a Path to the platform string format |
Path.pathSeparator |
IO Char |
The platform path separator: '/' on POSIX, '\\' on Windows |
Path.pathSeparators |
IO (List Char) |
All accepted separators: ['/'] on POSIX, ['\\', '/'] on Windows |
Path.toAbsoluteCwd |
Path → IO Path |
Resolve a relative path against the process CWD; no-op if already absolute; no symlink resolution |
Path.resolve |
Path → IO Path |
Make absolute and resolve all symlinks via uv_fs_realpath; fails if any component does not exist |
URI Integration
Init.System.Uri exposes two functions used by the LSP: pathToUri and fileUriToPath?. Both uses System.FilePath and will need to be updated with Std.Path. I thought about creating a dedicated Std.URI for them but it would require:
- Measure the impact on the LSP, given that changing structures (from String to a dedicated URI type) can lead to more memory comsumption, allocations and indirections.
- A dedicated implementation based on RFC 3986 and probably a broader approach to
Std.Http.Uriso it could be reused as part of theHttplibrary (given that Http URI implementation restricts a lot of URIs to avoid security problems)
| Name | Signature | Old equivalent |
|---|---|---|
Path.toFileUri |
Path → IO String |
System.Uri.pathToUri : FilePath → String |
Path.ofFileUri? |
String → IO (Option Path) |
System.Uri.fileUriToPath? : String → Option FilePath |
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the proposed API and migration plan, then inspect Init/System/FilePath.lean, Init/System/Uri.lean, and the path-related parts of Init/System/IO.lean. Done means implementing the proposed Std.Path representation and operations, then accounting for the dependent migration described in the issue; the proposal leaves usability details and the full deprecation timeline open.
Written by the indexing model from the issue text.
Assessment
- Domain
- operating-systems
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100