From cb60ad44894447a8dc2f5126f49e2b7e98609b2a Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Wed, 22 Jul 2026 13:32:05 +1000 Subject: [PATCH] Match basic-cli Path value behavior --- README.md | 7 +++++++ package/Path.roc | 45 +++++++++++++++++++++++++++++++++++++++++++++ package/main.roc | 10 +++++----- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7c69f1b..5472ad0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ main_file = Path.join("src", "main.roc") Quoted literals are stored as `Path.utf8` values. Platforms can convert the `Utf8` raw case to the host-native representation at the boundary. +Interpolated literals also produce UTF-8 paths and perform textual +concatenation. Use `Path.join` when adding a path component instead. + +`Path` equality and hashing preserve the exact tagged representation, so a +UTF-8 path is distinct from Unix bytes or Windows units with the same text. +`Str.inspect` identifies that representation without losing invalid raw units. + ## Examples Run an example with: diff --git a/package/Path.roc b/package/Path.roc index 7428347..058448b 100644 --- a/package/Path.roc +++ b/package/Path.roc @@ -12,6 +12,12 @@ Path :: [ from_quote : Str -> Try(Path, [BadQuotedBytes(Str)]) from_quote = |str| Ok(Utf8(str)) + ## Create a UTF-8 path from an interpolated string literal. + ## This performs textual concatenation; use [join] for path-component joining. + from_interpolation : Str, Iter((Str, Str)) -> Path + from_interpolation = |first, rest| + Utf8(rest.fold(first, |acc, (interpolated, segment)| acc.concat(interpolated).concat(segment))) + ## Create a Unix path from a Roc string by storing its UTF-8 bytes. unix : Str -> Path unix = |str| Unix(Str.to_utf8(str)) @@ -52,6 +58,23 @@ Path :: [ Windows(u16s) => Str.from_utf8_lossy(utf16_to_utf8_lossy(u16s)) } + ## Render a path for debugging without losing raw OS units. + to_inspect : Path -> Str + to_inspect = |path| + match path { + Utf8(str) => "Path.utf8(${Json.to_str(str)})" + Unix(bytes) => + match Str.from_utf8(bytes) { + Ok(str) => "Path.unix(${Json.to_str(str)})" + Err(_) => "Path.unix_bytes(${Str.inspect(bytes)})" + } + Windows(u16s) => + match utf16_to_str(u16s) { + Ok(str) => "Path.windows(${Json.to_str(str)})" + Err(_) => "Path.windows_u16s(${Str.inspect(u16s)})" + } + } + ## Returns everything after the last directory separator. filename : Path -> Try(Path, [IsDirPath, EndsInDots]) filename = |path| @@ -135,6 +158,9 @@ Path :: [ ## Compare paths by their exact tagged representation. is_eq : _ + + ## Hash paths consistently with exact tagged equality. + to_hash : _ } str_from_valid_utf8 : List(U8) -> Str @@ -350,6 +376,25 @@ quoted_literal_path = "config.txt" path_identity : Path -> Path path_identity = |path| path +## Interpolation creates a UTF-8 representation. +expect { + directory = "config" + path : Path + path = "${directory}/app.toml" + path == Path.utf8("config/app.toml") +} + +## Inspection identifies the representation and preserves invalid raw units. +expect Str.inspect(Path.utf8("a\nb")) == "Path.utf8(\"a\\nb\")" +expect Str.inspect(Path.unix("abc")) == "Path.unix(\"abc\")" +expect Str.inspect(Path.unix_bytes([97, 255, 98])) == "Path.unix_bytes([97, 255, 98])" +expect Str.inspect(Path.windows("abc")) == "Path.windows(\"abc\")" +expect Str.inspect(Path.windows_u16s([0xD800, 97])) == "Path.windows_u16s([55296, 97])" + +## Equality and hashing preserve representation identity. +expect Path.utf8("abc") != Path.unix("abc") +expect Dict.single(Path.unix_bytes([97, 255]), "found").get(Path.unix_bytes([97, 255])) == Ok("found") + ## Constructors preserve Unix, Windows, and UTF-8 path representations. expect Path.unix("abc") == Unix([97, 98, 99]) expect Path.unix_bytes([97, 98, 99]) == Unix([97, 98, 99]) diff --git a/package/main.roc b/package/main.roc index 021ed74..8c1041e 100644 --- a/package/main.roc +++ b/package/main.roc @@ -4,8 +4,8 @@ package ] {} -expect Str.inspect(Path.unix("abc")) == "" -expect Str.inspect(Path.unix_bytes([97, 98, 99])) == "" -expect Str.inspect(Path.windows("abc")) == "" -expect Str.inspect(Path.windows_u16s([97, 98, 99])) == "" -expect Str.inspect(Path.utf8("abc")) == "" +expect Str.inspect(Path.unix("abc")) == "Path.unix(\"abc\")" +expect Str.inspect(Path.unix_bytes([97, 98, 99])) == "Path.unix(\"abc\")" +expect Str.inspect(Path.windows("abc")) == "Path.windows(\"abc\")" +expect Str.inspect(Path.windows_u16s([97, 98, 99])) == "Path.windows(\"abc\")" +expect Str.inspect(Path.utf8("abc")) == "Path.utf8(\"abc\")"