leanprover / leanprover/lean4

RFC: Std.Path

Open
#13,922 4 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

P-high RFC
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.Uri so it could be reused as part of the Http library (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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.