From 3c5f29fbad0983920cf70d2e04c18ce05a920d73 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:15:04 +0100 Subject: [PATCH 01/11] update Roc version --- Cargo.lock | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- 2 files changed, 307 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32fd07dd..7862e4e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,95 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -37,6 +120,33 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memoffset" version = "0.9.1" @@ -46,6 +156,68 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "roc_command" version = "0.0.1" @@ -58,6 +230,7 @@ dependencies = [ name = "roc_host" version = "0.0.1" dependencies = [ + "crossterm", "libc", "memoffset", "roc_command", @@ -86,18 +259,100 @@ dependencies = [ [[package]] name = "roc_std_new" version = "0.0.1" -source = "git+https://github.com/roc-lang/roc?rev=36e9ff29fed0ebe6e9d47e30c14bc2d0a2545a5f#36e9ff29fed0ebe6e9d47e30c14bc2d0a2545a5f" +source = "git+https://github.com/roc-lang/roc?rev=a6e81e8a8ce50bbb3f00e7a553d08f2bb919e8e0#a6e81e8a8ce50bbb3f00e7a553d08f2bb919e8e0" dependencies = [ "arrayvec", "static_assertions", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -107,8 +362,57 @@ dependencies = [ "libc", ] +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index 47868e01..fb6603f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,8 +39,8 @@ repository = "https://github.com/roc-lang/basic-cli" [workspace.dependencies] # Core Roc types -# roc-nightly: 2026-01-12 -roc_std_new = { git = "https://github.com/roc-lang/roc", rev = "36e9ff29fed0ebe6e9d47e30c14bc2d0a2545a5f" } +# roc-nightly: 2026-02-09 +roc_std_new = { git = "https://github.com/roc-lang/roc", rev = "a6e81e8a8ce50bbb3f00e7a553d08f2bb919e8e0" } # Internal crates roc_io_error = { path = "crates/roc_io_error" } From dd1e0d4d7b7525e6a0fc533a43852aa821445b74 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:03:31 +0100 Subject: [PATCH 02/11] claude changes + Cmd manual changes --- Cargo.lock | 2 +- Cargo.toml | 4 +- ci/all_tests.sh | 4 +- examples/bytes-stdin-stdout.roc | 7 +- examples/command-line-args.roc | 5 +- examples/command.roc | 81 +++--- examples/dir.roc | 46 +-- examples/env-var.roc | 18 +- examples/error-handling.roc | 43 ++- examples/file-read-write.roc | 24 +- examples/hello-world.roc | 3 +- examples/locale.roc | 9 +- examples/path.roc | 3 +- examples/print.roc | 11 +- examples/random.roc | 4 +- examples/stdin-basic.roc | 17 +- examples/time.roc | 7 +- examples/tty.roc | 4 +- platform/Cmd.roc | 251 +++++++++++----- platform/CmdInternal.roc | 21 ++ platform/Env.roc | 12 +- platform/File.roc | 28 +- platform/IOErr.roc | 27 ++ platform/Locale.roc | 4 +- platform/Path.roc | 28 +- platform/Random.roc | 4 +- platform/Stderr.roc | 42 +-- platform/Stdin.roc | 50 +--- platform/Stdout.roc | 42 +-- platform/Utc.roc | 21 +- platform/main.roc | 11 +- src/lib.rs | 500 ++++++++++++++++++++++++-------- 32 files changed, 835 insertions(+), 498 deletions(-) create mode 100644 platform/CmdInternal.roc create mode 100644 platform/IOErr.roc diff --git a/Cargo.lock b/Cargo.lock index 7862e4e9..b6f576f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "roc_std_new" version = "0.0.1" -source = "git+https://github.com/roc-lang/roc?rev=a6e81e8a8ce50bbb3f00e7a553d08f2bb919e8e0#a6e81e8a8ce50bbb3f00e7a553d08f2bb919e8e0" +source = "git+https://github.com/roc-lang/roc?rev=c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f#c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f" dependencies = [ "arrayvec", "static_assertions", diff --git a/Cargo.toml b/Cargo.toml index fb6603f4..fb321be1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,8 +39,8 @@ repository = "https://github.com/roc-lang/basic-cli" [workspace.dependencies] # Core Roc types -# roc-nightly: 2026-02-09 -roc_std_new = { git = "https://github.com/roc-lang/roc", rev = "a6e81e8a8ce50bbb3f00e7a553d08f2bb919e8e0" } +# roc-nightly: 2026-02-20 +roc_std_new = { git = "https://github.com/roc-lang/roc", rev = "c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f" } # Internal crates roc_io_error = { path = "crates/roc_io_error" } diff --git a/ci/all_tests.sh b/ci/all_tests.sh index 503f8566..cbb062df 100755 --- a/ci/all_tests.sh +++ b/ci/all_tests.sh @@ -182,7 +182,7 @@ echo "" echo "=== Checking examples ===" for example in "${MIGRATED_EXAMPLES[@]}"; do echo "Checking: ${example}.roc" - roc check "examples/${example}.roc" + roc check --no-cache "examples/${example}.roc" done # roc build migrated examples @@ -194,7 +194,7 @@ else fi for example in "${MIGRATED_EXAMPLES[@]}"; do echo "Building: ${example}.roc" - roc build "examples/${example}.roc" + roc build --no-cache "examples/${example}.roc" mv "./${example}" "examples/" done diff --git a/examples/bytes-stdin-stdout.roc b/examples/bytes-stdin-stdout.roc index 11ca1a3d..98abca49 100644 --- a/examples/bytes-stdin-stdout.roc +++ b/examples/bytes-stdin-stdout.roc @@ -3,13 +3,12 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdin import pf.Stdout import pf.Stderr -import pf.Arg exposing [Arg] # To run this example: check the README.md in this folder -main! : List Arg => Result {} _ -main! = |_args| +main! = |_args| { data = Stdin.bytes!({})? Stderr.write_bytes!(data)? Stdout.write_bytes!(data)? - Ok {} + Ok({}) +} \ No newline at end of file diff --git a/examples/command-line-args.roc b/examples/command-line-args.roc index 3f92fee2..3220e34d 100644 --- a/examples/command-line-args.roc +++ b/examples/command-line-args.roc @@ -2,16 +2,15 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout -main! : List(Str) => Try({}, [Exit(I32)]) main! = |args| { # Skip first arg (executable path), get the remaining args match args.drop_first(1) { [first_arg, ..] => { - Stdout.line!("received argument: ${first_arg}") + Stdout.line!("received argument: ${first_arg}")? Ok({}) } [] => { - Stdout.line!("Error: I expected one argument, but got none.") + Stdout.line!("Error: I expected one argument, but got none.")? Err(Exit(1)) } } diff --git a/examples/command.roc b/examples/command.roc index 74a8a94c..025e0513 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -6,45 +6,44 @@ import pf.Cmd # Different ways to run commands like you do in a terminal. main! = |_args| { - # Simplest way to execute a command (prints to your terminal). - exec_result = Cmd.exec!("echo", ["Hello"]) - match exec_result { - Ok({}) => {} - Err(_) => Stdout.line!("Error running echo") - } - - # To execute and capture the output (stdout and stderr) without inheriting your terminal. - output_result = Cmd.exec_output!(Cmd.args(Cmd.new("echo"), ["Hi"])) - match output_result { - Ok(cmd_output) => Stdout.line!("{stderr_utf8_lossy: \"${cmd_output.stderr_utf8_lossy}\", stdout_utf8: \"${cmd_output.stdout_utf8}\"}") - Err(_) => Stdout.line!("Error capturing output") - } - - # To run a command with environment variables. - env_cmd = Cmd.args( - Cmd.envs( - Cmd.env( - Cmd.clear_envs(Cmd.new("env")), - "FOO", - "BAR", - ), - [("BAZ", "DUCK"), ("XYZ", "ABC")], - ), - ["-v"], - ) - env_result = Cmd.exec_cmd!(env_cmd) - match env_result { - Ok({}) => {} - Err(_) => Stdout.line!("Error running env") - } - - # To execute and just get the exit code (prints to your terminal). - # Prefer using `exec!` or `exec_cmd!`. - exit_result = Cmd.exec_exit_code!(Cmd.args(Cmd.new("cat"), ["non_existent.txt"])) - match exit_result { - Ok(exit_code) => Stdout.line!("Exit code: ${exit_code.to_str()}") - Err(_) => Stdout.line!("Error getting exit code") - } - - Ok({}) + # Simplest way to execute a command (prints to your terminal). + Cmd.exec!("echo", ["Hello"])? + + # To execute and capture the output (stdout and stderr) without inheriting your terminal. + cmd_output = + Cmd.new("echo") + .args(["Hi"]) + .exec_output!()? + + Stdout.line!("${Str.inspect(cmd_output)}")? + + # To run a command with environment variables. + Cmd.new("env") + .clear_envs() # You probably don't need to clear all other environment variables, this is just an example. + .env("FOO", "BAR") + .envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs` + .args(["-v"]) + .exec_cmd!()? + + # To execute and just get the exit code (prints to your terminal). + # Prefer using `exec!` or `exec_cmd!`. + exit_code = + Cmd.new("cat") + .args(["non_existent.txt"]) + .exec_exit_code!()? + + Stdout.line!("Exit code: ${exit_code.to_str()}")? + + # TODO add exec_output_bytes + + # To execute and capture the output (stdout and stderr) in the original form as bytes without inheriting your terminal. + # Prefer using `exec_output!`. + #cmd_output_bytes = + # Cmd.new("echo") + # .args(["Hi"]) + # .exec_output_bytes!()? + + #Stdout.line!("${Str.inspect(cmd_output_bytes)}")? + + Ok({}) } diff --git a/examples/dir.roc b/examples/dir.roc index 249e1156..19b17987 100644 --- a/examples/dir.roc +++ b/examples/dir.roc @@ -6,30 +6,40 @@ import pf.Dir # Demo of all Dir functions. main! = |_args| { - # Create a directory - Dir.create!("empty-dir")? + dir_result = { + # Create a directory + Dir.create!("empty-dir")? - # Create a directory and its parents - Dir.create_all!("nested-dir/a/b/c")? + # Create a directory and its parents + Dir.create_all!("nested-dir/a/b/c")? - # Create a child directory - Dir.create!("nested-dir/child")? + # Create a child directory + Dir.create!("nested-dir/child")? - # List the contents of a directory - paths = Dir.list!("nested-dir")? + # List the contents of a directory + paths = Dir.list!("nested-dir")? - # Check the contents of the directory - expect List.len(paths) == 2 - expect List.contains(paths, "nested-dir/a") - expect List.contains(paths, "nested-dir/child") + # Check the contents of the directory + expect List.len(paths) == 2 + expect List.contains(paths, "nested-dir/a") + expect List.contains(paths, "nested-dir/child") - # Delete an empty directory - Dir.delete_empty!("empty-dir")? + # Delete an empty directory + Dir.delete_empty!("empty-dir")? - # Delete all directories recursively - Dir.delete_all!("nested-dir")? + # Delete all directories recursively + Dir.delete_all!("nested-dir")? - Stdout.line!("Success!") + _r = Stdout.line!("Success!") - Ok({}) + Ok({}) + } + + match dir_result { + Ok({}) => Ok({}) + Err(_) => { + _r = Stdout.line!("Error during directory operations") + Err(Exit(1)) + } + } } diff --git a/examples/env-var.roc b/examples/env-var.roc index d56c8c1a..04cfc0ab 100644 --- a/examples/env-var.roc +++ b/examples/env-var.roc @@ -5,15 +5,17 @@ import pf.Env # How to read environment variables with Env.var! -main! : List(Str) => Try({}, [Exit(I32)]) main! = |_args| { - editor = Env.var!("EDITOR") + result = Env.var!("EDITOR") - if Str.is_empty(editor) { - Stdout.line!("EDITOR is not set") - } else { - Stdout.line!("Your favorite editor is ${editor}!") + match result { + Ok(editor) => { + _r = Stdout.line!("Your favorite editor is ${editor}!") + Ok({}) + } + Err(VarNotFound(name)) => { + _r = Stdout.line!("${name} is not set") + Ok({}) + } } - - Ok({}) } diff --git a/examples/error-handling.roc b/examples/error-handling.roc index 51eaa0cd..7e9b56f4 100644 --- a/examples/error-handling.roc +++ b/examples/error-handling.roc @@ -11,22 +11,41 @@ main! = |_args| { # Try to read a file that doesn't exist - should error result = File.read_utf8!("nonexistent-file.txt") match result { - Ok(content) => Stdout.line!("Unexpected success: ${content}") - Err(FileErr(NotFound)) => Stdout.line!("Expected error: File not found (NotFound)") - Err(FileErr(PermissionDenied)) => Stdout.line!("Error: Permission denied") - Err(FileErr(Other(msg))) => Stdout.line!("Error: ${msg}") - Err(_) => Stdout.line!("Error: Other file error") + Ok(content) => { + _r = Stdout.line!("Unexpected success: ${content}") + } + Err(FileErr(NotFound)) => { + _r = Stdout.line!("Expected error: File not found (NotFound)") + } + Err(FileErr(PermissionDenied)) => { + _r = Stdout.line!("Error: Permission denied") + } + Err(FileErr(Other(msg))) => { + _r = Stdout.line!("Error: ${msg}") + } + Err(_) => { + _r = Stdout.line!("Error: Other file error") + } } # Now demonstrate success path - create, read, then cleanup - # Using ? operator to propagate errors (works with open tag unions) - File.write_utf8!(file_name, "Hello from error-handling example!")? + file_result = { + File.write_utf8!(file_name, "Hello from error-handling example!")? - content = File.read_utf8!(file_name)? - Stdout.line!("${file_name} contains: ${content}") + content = File.read_utf8!(file_name)? + _r = Stdout.line!("${file_name} contains: ${content}") - # Cleanup - File.delete!(file_name)? + # Cleanup + File.delete!(file_name)? - Ok({}) + Ok({}) + } + + match file_result { + Ok({}) => Ok({}) + Err(_) => { + _r = Stdout.line!("Error during file operations") + Err(Exit(1)) + } + } } diff --git a/examples/file-read-write.roc b/examples/file-read-write.roc index 9410a3b4..6559cb04 100644 --- a/examples/file-read-write.roc +++ b/examples/file-read-write.roc @@ -8,16 +8,26 @@ import pf.File main! = |_args| { out_file = "out.txt" - Stdout.line!("Writing a string to out.txt") + _r = Stdout.line!("Writing a string to out.txt") - File.write_utf8!(out_file, "a string!")? + result = { + File.write_utf8!(out_file, "a string!")? - contents = File.read_utf8!(out_file)? + contents = File.read_utf8!(out_file)? - Stdout.line!("I read the file back. Its contents are: \"${contents}\"") + _r = Stdout.line!("I read the file back. Its contents are: \"${contents}\"") - # Cleanup - File.delete!(out_file)? + # Cleanup + File.delete!(out_file)? - Ok({}) + Ok({}) + } + + match result { + Ok({}) => Ok({}) + Err(_) => { + _r = Stdout.line!("Error during file operations") + Err(Exit(1)) + } + } } diff --git a/examples/hello-world.roc b/examples/hello-world.roc index 3c4c1d42..9734e890 100644 --- a/examples/hello-world.roc +++ b/examples/hello-world.roc @@ -2,8 +2,7 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdout -main! : List(Str) => Try({}, [Exit(I32)]) main! = |_args| { - Stdout.line!("Hello, World!") + _r = Stdout.line!("Hello, World!") Ok({}) } diff --git a/examples/locale.roc b/examples/locale.roc index 541ea628..98337d65 100644 --- a/examples/locale.roc +++ b/examples/locale.roc @@ -6,12 +6,15 @@ import pf.Locale # Getting the preferred locale and all available locales main! = |_args| { - locale_str = Locale.get!() - Stdout.line!("The most preferred locale for this system or application: ${locale_str}") + locale_str = match Locale.get!() { + Ok(locale) => locale + Err(NotAvailable) => "" + } + match Stdout.line!("The most preferred locale for this system or application: ${locale_str}") { _ => {} } all_locales = Locale.all!() locales_str = Str.join_with(all_locales, ", ") - Stdout.line!("All available locales for this system or application: [${locales_str}]") + match Stdout.line!("All available locales for this system or application: [${locales_str}]") { _ => {} } Ok({}) } diff --git a/examples/path.roc b/examples/path.roc index 47cec5dd..9173c56f 100644 --- a/examples/path.roc +++ b/examples/path.roc @@ -5,7 +5,6 @@ import pf.Path # Demo of basic-cli Path functions -main! : List(Str) => Try({}, [Exit(I32)]) main! = |_args| { path = "path.roc" @@ -13,7 +12,7 @@ main! = |_args| { b = Path.is_dir!(path) c = Path.is_sym_link!(path) - Stdout.line!( + _r = Stdout.line!( \\is_file: ${Str.inspect(a)} \\is_dir: ${Str.inspect(b)} \\is_sym_link: ${Str.inspect(c)} diff --git a/examples/print.roc b/examples/print.roc index b0383227..1cfe5f53 100644 --- a/examples/print.roc +++ b/examples/print.roc @@ -5,23 +5,22 @@ import pf.Stderr # Printing to stdout and stderr -main! : List(Str) => Try({}, [Exit(I32)]) main! = |_args| { # Print a string to stdout - Stdout.line!("Hello, world!") + match Stdout.line!("Hello, world!") { _ => {} } # Print without a newline - Stdout.write!("No newline after me.") + match Stdout.write!("No newline after me.") { _ => {} } # Print a string to stderr - Stderr.line!("Hello, error!") + match Stderr.line!("Hello, error!") { _ => {} } # Print a string to stderr without a newline - Stderr.write!("Err with no newline after.") + match Stderr.write!("Err with no newline after.") { _ => {} } # Print a list to stdout List.for_each!(["Foo", "Bar", "Baz"], |str| { - Stdout.line!(str) + match Stdout.line!(str) { _ => {} } }) Ok({}) diff --git a/examples/random.roc b/examples/random.roc index 7b0871d5..f4883b2b 100644 --- a/examples/random.roc +++ b/examples/random.roc @@ -9,11 +9,11 @@ main! = |_args| { result = Random.seed_u64!({}) match result { Ok(random_u64) => { - Stdout.line!("Random U64 seed is: ${random_u64.to_str()}") + _r = Stdout.line!("Random U64 seed is: ${random_u64.to_str()}") Ok({}) } Err(_) => { - Stdout.line!("Failed to generate random seed") + _r = Stdout.line!("Failed to generate random seed") Err(Exit(1)) } } diff --git a/examples/stdin-basic.roc b/examples/stdin-basic.roc index facc7d3f..1544abf9 100644 --- a/examples/stdin-basic.roc +++ b/examples/stdin-basic.roc @@ -3,14 +3,19 @@ app [main!] { pf: platform "../platform/main.roc" } import pf.Stdin import pf.Stdout -main! : List(Str) => Try({}, [Exit(I32)]) main! = |_args| { - Stdout.line!("What's your first name?") - first = Stdin.line!({}) + match Stdout.line!("What's your first name?") { _ => {} } + first = match Stdin.line!({}) { + Ok(line) => line + Err(_) => "" + } - Stdout.line!("What's your last name?") - last = Stdin.line!({}) + match Stdout.line!("What's your last name?") { _ => {} } + last = match Stdin.line!({}) { + Ok(line) => line + Err(_) => "" + } - Stdout.line!("Hi, ${first} ${last}! \u(1F44B)") + match Stdout.line!("Hi, ${first} ${last}! \u(1F44B)") { _ => {} } Ok({}) } diff --git a/examples/time.roc b/examples/time.roc index 403faa20..ea8adcc2 100644 --- a/examples/time.roc +++ b/examples/time.roc @@ -6,7 +6,6 @@ import pf.Sleep # Demo Utc and Sleep functions -main! : List(Str) => Try({}, [Exit(I32)]) main! = |_args| { start = Utc.now!({}) @@ -15,10 +14,10 @@ main! = |_args| { finish = Utc.now!({}) - duration_nanos = finish - start - duration_ms = duration_nanos // 1_000_000 + duration_ms = Utc.delta_as_millis(finish, start) + duration_nanos = Utc.delta_as_nanos(finish, start) - Stdout.line!("Completed in ${duration_ms.to_str()} ms (${duration_nanos.to_str()} ns)") + _r = Stdout.line!("Completed in ${duration_ms.to_str()} ms (${duration_nanos.to_str()} ns)") Ok({}) } diff --git a/examples/tty.roc b/examples/tty.roc index 5e18476b..8c30b1b9 100644 --- a/examples/tty.roc +++ b/examples/tty.roc @@ -7,10 +7,10 @@ import pf.Tty ## This is useful for running an app like vim or a game in the terminal. main! = |_args| { - Stdout.line!("Tty: enabling raw mode") + match Stdout.line!("Tty: enabling raw mode") { _ => {} } Tty.enable_raw_mode!() - Stdout.line!("Tty: disabling raw mode") + match Stdout.line!("Tty: disabling raw mode") { _ => {} } Tty.disable_raw_mode!() Ok({}) diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 109710de..3da42978 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -1,12 +1,163 @@ +import IOErr exposing [IOErr] +import CmdInternal + Cmd :: { args : List(Str), clear_envs : Bool, envs : List(Str), program : Str, }.{ - IOErr := [NotFound, PermissionDenied, BrokenPipe, AlreadyExists, Interrupted, Unsupported, OutOfMemory, Other(Str)] - ## Create a new command with the given program name. + ## Simplest way to execute a command by name with arguments. + ## Stdin, stdout, and stderr are inherited from the parent process. + ## + ## If you want to capture the output, use [exec_output!] instead. + ## + ## ```roc + ## Cmd.exec!("echo", ["hello world"])? + ## ``` + exec! : Str, List(Str) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode { command : Str, err : IOErr }, ..]) + exec! = |program, arguments| { + exit_code = + new(program) + .args(arguments) + .exec_exit_code!()? + + if exit_code == 0 { + Ok({}) + } else { + command = "${cmd_name} ${arguments.join_with(" ")}" + Err(ExecFailed({ command, exit_code })) + } + } + + ## Execute a Cmd (using the builder pattern). + ## Stdin, stdout, and stderr are inherited from the parent process. + ## + ## You should prefer using [exec!] instead, only use this if you want to use [env], [envs] or [clear_envs]. + ## If you want to capture the output, use [exec_output!] instead. + ## + ## ```roc + ## Cmd.new("cargo") + ## .arg(["build") + ## .env("RUST_BACKTRACE", "1") + ## .exec_cmd!()? + ## ``` + exec_cmd! : Cmd => Try({}, [ExecCmdFailed { command : Str, exit_code : I32 }, FailedToGetExitCode { command : Str, err : IOErr }, ..]) + exec_cmd! = |cmd| { + exit_code = exec_exit_code!(cmd)? + + if exit_code == 0 { + Ok({}) + } else { + Err(ExecCmdFailed({ command: to_str(cmd), exit_code })) + } + } + + ## Execute command and capture stdout and stderr as UTF-8 strings. + ## Invalid UTF-8 sequences are replaced with the Unicode replacement character. + ## + ## Use [exec_output_bytes!] instead if you want to capture the output in the original form as bytes. + ## [exec_output_bytes!] may also be used for maximum performance, because you may be able to avoid unnecessary UTF-8 conversions. + ## + ## ```roc + ## cmd_output = + ## Cmd.new("echo") + ## .args(["Hi"]) + ## .exec_output!()? + ## + ## Stdout.line!("Echo output: ${cmd_output.stdout_utf8}")? + ## ``` + exec_output! : Cmd => Try( + { stdout_utf8 : Str, stderr_utf8_lossy : Str }, + [ + StdoutContainsInvalidUtf8({ cmd_str : Str, err : [BadUtf8 { index : U64, problem : Str.Utf8Problem }] }), + NonZeroExitCode({ command : Str, exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str }), + FailedToGetExitCode({ command : Str, err : IOErr }), + .. + ] + ) + exec_output! = |cmd| + exec_try = CmdInternal.command_exec_output!(cmd) + + match exec_try { + Ok({ stderr_bytes, stdout_bytes }) => + stdout_utf8 = + Str.from_utf8(stdout_bytes) + .map_err(|err| StdoutContainsInvalidUtf8({ cmd_str: to_str(cmd), err }))? + + stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) + + Ok({ stdout_utf8, stderr_utf8_lossy }) + + Err(inside_try) => + match inside_try { + Ok({ exit_code, stderr_bytes, stdout_bytes }) => + stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes) + stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) + + Err(NonZeroExitCode({ command: to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy })) + + Err(err) => + Err(FailedToGetExitCode({ command: to_str(cmd), err: InternalIOErr.handle_err(err) })) + } + } + + ## Execute command and capture stdout and stderr in the original form as bytes. + ## + ## Use [exec_output!] instead if you want to get the output as UTF-8 strings. + ## + ## ```roc + ## cmd_output = + ## Cmd.new("echo") + ## .args(["Hi"]) + ## .exec_output_bytes!()? + ## + ## Stdout.line!("${Str.inspect(cmd_output_bytes)}")? # {stderr_bytes: [], stdout_bytes: [72, 105, 10]} + ## ``` + exec_output_bytes! : Cmd => Try( + { stderr_bytes : List(U8), stdout_bytes : List(U8) } + [ + FailedToGetExitCodeB(IOErr), # TODO: perhaps no need for B? + NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) }), + .. + ] + ) + exec_output_bytes! = |cmd| { + exec_try = CmdInternal.command_exec_output!(cmd) # TODO + + match exec_try { + Ok({ stderr_bytes, stdout_bytes }) => + Ok({ stdout_bytes, stderr_bytes }) + + Err(inside_try) => + match inside_try { + Ok({ exit_code, stderr_bytes, stdout_bytes }) -> + Err(NonZeroExitCodeB({ exit_code, stdout_bytes, stderr_bytes })) + + Err(err) -> + Err(FailedToGetExitCodeB(InternalIOErr.handle_err(err))) + } + } + } + + ## Execute a command and return its exit code. + ## Stdin, stdout, and stderr are inherited from the parent process. + ## + ## You should prefer using [exec!] or [exec_cmd!] instead, only use this if you want to take a specific action based on a **specific non-zero exit code**. + ## For example, `roc check` returns exit code 1 if there are errors, and exit code 2 if there are only warnings. + ## So, you could use `exec_exit_code!` to ignore warnings on `roc check`. + ## + ## ```roc + ## exit_code = Cmd.new("cat").arg("non_existent.txt").exec_exit_code!()? + ## ``` + exec_exit_code! : Cmd => Try(I32, [FailedToGetExitCode { command : Str, err : IOErr }, ..]) + exec_exit_code! = |cmd| { + CmdInternal.command_exec_exit_code!(cmd) # TODO + .map_err(|err| FailedToGetExitCode({ command: to_str(cmd), err })) + } + + ## Create a new command with the given program name. Use a function that starts with `exec_` to execute it. ## ## ```roc ## cmd = Cmd.new("ls") @@ -20,42 +171,39 @@ Cmd :: { } ## Add a single argument to the command. + ## ❗ Shell features like variable subsitition (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. ## ## ```roc ## cmd = Cmd.new("ls").arg("-l") ## ``` arg : Cmd, Str -> Cmd arg = |cmd, a| { - args: List.append(cmd.args, a), - clear_envs: cmd.clear_envs, - envs: cmd.envs, - program: cmd.program, + ..cmd, + args: cmd.args.append(a), } ## Add multiple arguments to the command. + ## ❗ Shell features like variable subsitition (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. ## ## ```roc ## cmd = Cmd.new("ls").args(["-l", "-a"]) ## ``` args : Cmd, List(Str) -> Cmd args = |cmd, new_args| { - args: List.concat(cmd.args, new_args), - clear_envs: cmd.clear_envs, - envs: cmd.envs, - program: cmd.program, + ..cmd, + args: cmd.args.concat(new_args), } ## Add a single environment variable to the command. + ## ## ## ```roc - ## cmd = Cmd.new("env").env("FOO", "bar") + ## cmd = Cmd.new("env").env("FOO", "bar") # add the environment variable "FOO" with value "bar" ## ``` env : Cmd, Str, Str -> Cmd env = |cmd, key, value| { - args: cmd.args, - clear_envs: cmd.clear_envs, - envs: List.concat(cmd.envs, [key, value]), - program: cmd.program, + ..cmd, + envs: cmd.envs.concat([key, value]), } ## Add multiple environment variables to the command. @@ -65,20 +213,22 @@ Cmd :: { ## ``` envs : Cmd, List((Str, Str)) -> Cmd envs = |cmd, pairs| { - flat = List.fold(pairs, [], |acc, (k, v)| List.concat(acc, [k, v])) + flat = pairs.fold([], |acc, (k, v)| acc.concat([k, v])) { - args: cmd.args, - clear_envs: cmd.clear_envs, - envs: List.concat(cmd.envs, flat), - program: cmd.program, + ..cmd, + envs: cmd.envs.concat(flat), } } ## Clear all environment variables before running the command. ## Only environment variables added via `env` or `envs` will be available. + ## Useful if you want a clean command run that does not behave unexpectedly if the user has some env var set. ## ## ```roc - ## cmd = Cmd.new("env").clear_envs().env("ONLY_THIS", "visible") + ## cmd = + ## Cmd.new("env") + ## .clear_envs() + ## .env("ONLY_THIS", "visible") ## ``` clear_envs : Cmd -> Cmd clear_envs = |cmd| { @@ -87,63 +237,4 @@ Cmd :: { envs: cmd.envs, program: cmd.program, } - - ## Execute a command and return its exit code. - ## Stdin, stdout, and stderr are inherited from the parent process. - ## - ## ```roc - ## exit_code = Cmd.new("ls").arg("-l").exec_exit_code!()? - ## ``` - exec_exit_code! : Cmd => Try(I32, [CmdErr(IOErr)]) - - ## Execute command and capture stdout/stderr as UTF-8 strings. - ## Invalid UTF-8 sequences are replaced with the Unicode replacement character. - ## - ## ```roc - ## cmd_output = - ## Cmd.new("echo") - ## .args(["Hi"]) - ## .exec_output!()? - ## - ## Stdout.line!("Echo output: ${cmd_output.stdout_utf8}")? - ## ``` - exec_output! : Cmd => Try( - { stdout_utf8 : Str, stderr_utf8_lossy : Str }, - [CmdErr(IOErr), NonZeroExit({ exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str })] - ) - - ## Simple helper to execute a command by name with arguments. - ## Stdin, stdout, and stderr are inherited from the parent process. - ## Returns Ok if the command exits with code 0. - ## - ## ```roc - ## Cmd.exec!("ls", ["-l", "-a"])? - ## ``` - exec! : Str, List(Str) => Try({}, [CmdErr(IOErr), ExecFailed({ command : Str, exit_code : I32 })]) - exec! = |program, arguments| { - cmd = new(program).args(arguments) - result = exec_exit_code!(cmd) - match result { - Ok(0) => Ok({}), - Ok(exit_code) => Err(ExecFailed({ command: program, exit_code })), - Err(CmdErr(io_err)) => Err(CmdErr(io_err)), - } - } - - ## Execute a command using the builder pattern. - ## Stdin, stdout, and stderr are inherited from the parent process. - ## Returns Ok if the command exits with code 0. - ## - ## ```roc - ## Cmd.new("ls").args(["-l", "-a"]).exec_cmd!()? - ## ``` - exec_cmd! : Cmd => Try({}, [CmdErr(IOErr), ExecFailed({ exit_code : I32 })]) - exec_cmd! = |cmd| { - result = exec_exit_code!(cmd) - match result { - Ok(0) => Ok({}), - Ok(code) => Err(ExecFailed({ exit_code: code })), - Err(CmdErr(io_err)) => Err(CmdErr(io_err)), - } - } } diff --git a/platform/CmdInternal.roc b/platform/CmdInternal.roc new file mode 100644 index 00000000..91ea2283 --- /dev/null +++ b/platform/CmdInternal.roc @@ -0,0 +1,21 @@ +import Cmd exposing [Cmd] +import IOErr exposing [IOErr] + +CmdInternal :: [].{ + command_exec_exit_code! : Cmd => Try(I32, IOErr) + + command_exec_output! : Cmd => Try(OutputFromHostSuccess, (Try(OutputFromHostFailure, IOErr))) +} + +# Do not change the order of the fields! It will lead to a segfault. +OutputFromHostSuccess : { + stderr_bytes : List(U8), + stdout_bytes : List(U8), +} + +# Do not change the order of the fields! It will lead to a segfault. +OutputFromHostFailure : { + stderr_bytes : List(U8), + stdout_bytes : List(U8), + exit_code : I32, +} \ No newline at end of file diff --git a/platform/Env.roc b/platform/Env.roc index 2047f681..7949c744 100644 --- a/platform/Env.roc +++ b/platform/Env.roc @@ -4,17 +4,17 @@ Env := [].{ ## If the value is invalid Unicode, the invalid parts will be replaced with the ## [Unicode replacement character](https://unicode.org/glossary/#replacement_character). ## - ## Returns an empty string if the variable is not found. - var! : Str => Str + ## Returns `Err(VarNotFound(name))` if the variable is not set. + var! : Str => Try(Str, [VarNotFound(Str)]) ## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory) ## from the environment. ## - ## Returns an empty string if the cwd is unavailable. - cwd! : {} => Str + ## Returns `Err(CwdUnavailable)` if the cwd cannot be determined. + cwd! : {} => Try(Str, [CwdUnavailable]) ## Gets the path to the currently-running executable. ## - ## Returns an empty string if the path is unavailable. - exe_path! : {} => Str + ## Returns `Err(ExePathUnavailable)` if the path cannot be determined. + exe_path! : {} => Try(Str, [ExePathUnavailable]) } diff --git a/platform/File.roc b/platform/File.roc index 9e2c0749..e02cfece 100644 --- a/platform/File.roc +++ b/platform/File.roc @@ -1,30 +1,6 @@ -File := [].{ - ## **NotFound** - An entity was not found, often a file. - ## - ## **PermissionDenied** - The operation lacked the necessary privileges to complete. - ## - ## **BrokenPipe** - The operation failed because a pipe was closed. - ## - ## **AlreadyExists** - An entity already exists, often a file. - ## - ## **Interrupted** - This operation was interrupted. Interrupted operations can typically be retried. - ## - ## **Unsupported** - This operation is unsupported on this platform. This means that the operation can never succeed. - ## - ## **OutOfMemory** - An operation could not be completed, because it failed to allocate enough memory. - ## - ## **Other** - A custom error that does not fall under any other I/O error kind. - IOErr := [ - NotFound, - PermissionDenied, - BrokenPipe, - AlreadyExists, - Interrupted, - Unsupported, - OutOfMemory, - Other(Str), - ] +import IOErr exposing [IOErr] +File := [].{ ## Read all bytes from a file. read_bytes! : Str => Try(List(U8), [FileErr(IOErr)]) diff --git a/platform/IOErr.roc b/platform/IOErr.roc new file mode 100644 index 00000000..68e33f14 --- /dev/null +++ b/platform/IOErr.roc @@ -0,0 +1,27 @@ +## Represents an I/O error that can occur during platform operations. +## +## **NotFound** - An entity was not found, often a file. +## +## **PermissionDenied** - The operation lacked the necessary privileges to complete. +## +## **BrokenPipe** - The operation failed because a pipe was closed. +## +## **AlreadyExists** - An entity already exists, often a file. +## +## **Interrupted** - This operation was interrupted. Interrupted operations can typically be retried. +## +## **Unsupported** - This operation is unsupported on this platform. This means that the operation can never succeed. +## +## **OutOfMemory** - An operation could not be completed, because it failed to allocate enough memory. +## +## **Other** - A custom error that does not fall under any other I/O error kind. +IOErr := [ + AlreadyExists, + BrokenPipe, + Interrupted, + NotFound, + Other(Str), + OutOfMemory, + PermissionDenied, + Unsupported, +] diff --git a/platform/Locale.roc b/platform/Locale.roc index 6598902c..2d7ced38 100644 --- a/platform/Locale.roc +++ b/platform/Locale.roc @@ -2,7 +2,9 @@ Locale := [].{ ## Returns the most preferred locale for the system or application. ## ## The returned [Str] is a BCP 47 language tag, like `en-US` or `fr-CA`. - get! : () => Str + ## + ## Returns `Err(NotAvailable)` if the locale cannot be determined. + get! : () => Try(Str, [NotAvailable]) ## Returns the preferred locales for the system or application. ## diff --git a/platform/Path.roc b/platform/Path.roc index 8c8ba084..941b9e18 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -1,30 +1,6 @@ -Path := [].{ - ## **NotFound** - An entity was not found, often a file. - ## - ## **PermissionDenied** - The operation lacked the necessary privileges to complete. - ## - ## **BrokenPipe** - The operation failed because a pipe was closed. - ## - ## **AlreadyExists** - An entity already exists, often a file. - ## - ## **Interrupted** - This operation was interrupted. Interrupted operations can typically be retried. - ## - ## **Unsupported** - This operation is unsupported on this platform. - ## - ## **OutOfMemory** - An operation could not be completed, because it failed to allocate enough memory. - ## - ## **Other** - A custom error that does not fall under any other I/O error kind. - IOErr := [ - NotFound, - PermissionDenied, - BrokenPipe, - AlreadyExists, - Interrupted, - Unsupported, - OutOfMemory, - Other(Str), - ] +import IOErr exposing [IOErr] +Path := [].{ ## Returns `Bool.true` if the path exists on disk and is pointing at a regular file. ## ## This function will traverse symbolic links to query information about the diff --git a/platform/Random.roc b/platform/Random.roc index 350ca442..fc409c3b 100644 --- a/platform/Random.roc +++ b/platform/Random.roc @@ -1,6 +1,6 @@ -Random := [].{ - IOErr := [NotFound, PermissionDenied, BrokenPipe, AlreadyExists, Interrupted, Unsupported, OutOfMemory, Other(Str)] +import IOErr exposing [IOErr] +Random := [].{ ## Generate a random 64-bit unsigned integer seed. seed_u64! : {} => Try(U64, [RandomErr(IOErr)]) diff --git a/platform/Stderr.roc b/platform/Stderr.roc index 847b0bad..a823020a 100644 --- a/platform/Stderr.roc +++ b/platform/Stderr.roc @@ -1,35 +1,11 @@ -Stderr := [].{ - ## **NotFound** - An entity was not found, often a file. - ## - ## **PermissionDenied** - The operation lacked the necessary privileges to complete. - ## - ## **BrokenPipe** - The operation failed because a pipe was closed. - ## - ## **AlreadyExists** - An entity already exists, often a file. - ## - ## **Interrupted** - This operation was interrupted. Interrupted operations can typically be retried. - ## - ## **Unsupported** - This operation is unsupported on this platform. This means that the operation can never succeed. - ## - ## **OutOfMemory** - An operation could not be completed, because it failed to allocate enough memory. - ## - ## **Other** - A custom error that does not fall under any other I/O error kind. - IOErr := [ - NotFound, - PermissionDenied, - BrokenPipe, - AlreadyExists, - Interrupted, - Unsupported, - OutOfMemory, - Other(Str), - ] +import IOErr exposing [IOErr] +Stderr := [].{ ## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)), ## followed by a newline. ## ## > To write to `stderr` without the newline, see [Stderr.write!]. - line! : Str => {} + line! : Str => Try({}, [StderrErr(IOErr), ..]) ## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). ## @@ -37,11 +13,11 @@ Stderr := [].{ ## so this may appear to do nothing until you write a newline! ## ## > To write to `stderr` with a newline at the end, see [Stderr.line!]. - write! : Str => {} + write! : Str => Try({}, [StderrErr(IOErr), ..]) - # ## Write the given bytes to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). - # ## - # ## Most terminals will not actually display content that are written to them until they receive a newline, - # ## so this may appear to do nothing until you write a newline! - # write_bytes! : List(U8) => {} + ## Write the given bytes to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). + ## + ## Most terminals will not actually display content that are written to them until they receive a newline, + ## so this may appear to do nothing until you write a newline! + write_bytes! : List(U8) => Try({}, [StderrErr(IOErr), ..]) } diff --git a/platform/Stdin.roc b/platform/Stdin.roc index 094fd2fb..46da4e33 100644 --- a/platform/Stdin.roc +++ b/platform/Stdin.roc @@ -1,47 +1,23 @@ -Stdin := [].{ - ## **NotFound** - An entity was not found, often a file. - ## - ## **PermissionDenied** - The operation lacked the necessary privileges to complete. - ## - ## **BrokenPipe** - The operation failed because a pipe was closed. - ## - ## **AlreadyExists** - An entity already exists, often a file. - ## - ## **Interrupted** - This operation was interrupted. Interrupted operations can typically be retried. - ## - ## **Unsupported** - This operation is unsupported on this platform. This means that the operation can never succeed. - ## - ## **OutOfMemory** - An operation could not be completed, because it failed to allocate enough memory. - ## - ## **Other** - A custom error that does not fall under any other I/O error kind. - IOErr := [ - NotFound, - PermissionDenied, - BrokenPipe, - AlreadyExists, - Interrupted, - Unsupported, - OutOfMemory, - Other(Str), - ] +import IOErr exposing [IOErr] +Stdin := [].{ ## Read a line from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). ## ## > This task will block the program from continuing until `stdin` receives a newline character ## (e.g. because the user pressed Enter in the terminal), so using it can result in the appearance of the ## program having gotten stuck. It's often helpful to print a prompt first, so ## the user knows it's necessary to enter something before the program will continue. - line! : {} => Str + line! : {} => Try(Str, [EndOfFile, StdinErr(IOErr), ..]) - # ## Read bytes from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). - # ## This function can read no more than 16,384 bytes at a time. Use [read_to_end!] if you need more. - # ## - # ## > This is typically used in combintation with [Tty.enable_raw_mode!], - # ## which disables defaults terminal bevahiour and allows reading input - # ## without buffering until Enter key is pressed. - # bytes! : {} => List(U8) + ## Read bytes from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). + ## This function can read no more than 16,384 bytes at a time. Use [read_to_end!] if you need more. + ## + ## > This is typically used in combintation with [Tty.enable_raw_mode!], + ## which disables defaults terminal bevahiour and allows reading input + ## without buffering until Enter key is pressed. + bytes! : {} => Try(List(U8), [EndOfFile, StdinErr(IOErr), ..]) - # ## Read all bytes from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)) - # ## until [EOF](https://en.wikipedia.org/wiki/End-of-file) in this source. - # read_to_end! : {} => List(U8) + ## Read all bytes from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)) + ## until [EOF](https://en.wikipedia.org/wiki/End-of-file) in this source. + read_to_end! : {} => Try(List(U8), [StdinErr(IOErr), ..]) } diff --git a/platform/Stdout.roc b/platform/Stdout.roc index 01a2e83a..83ca3b52 100644 --- a/platform/Stdout.roc +++ b/platform/Stdout.roc @@ -1,35 +1,11 @@ -Stdout := [].{ - ## **NotFound** - An entity was not found, often a file. - ## - ## **PermissionDenied** - The operation lacked the necessary privileges to complete. - ## - ## **BrokenPipe** - The operation failed because a pipe was closed. - ## - ## **AlreadyExists** - An entity already exists, often a file. - ## - ## **Interrupted** - This operation was interrupted. Interrupted operations can typically be retried. - ## - ## **Unsupported** - This operation is unsupported on this platform. This means that the operation can never succeed. - ## - ## **OutOfMemory** - An operation could not be completed, because it failed to allocate enough memory. - ## - ## **Other** - A custom error that does not fall under any other I/O error kind. - IOErr := [ - NotFound, - PermissionDenied, - BrokenPipe, - AlreadyExists, - Interrupted, - Unsupported, - OutOfMemory, - Other(Str), - ] +import IOErr exposing [IOErr] +Stdout := [].{ ## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), ## followed by a newline. ## ## > To write to `stdout` without the newline, see [Stdout.write!]. - line! : Str => {} + line! : Str => Try({}, [StdoutErr(IOErr), ..]) ## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). ## @@ -37,11 +13,11 @@ Stdout := [].{ ## so this may appear to do nothing until you write a newline! ## ## > To write to `stdout` with a newline at the end, see [Stdout.line!]. - write! : Str => {} + write! : Str => Try({}, [StdoutErr(IOErr), ..]) - # ## Write the given bytes to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). - # ## - # ## Note that many terminals will not actually display content that is written to them until they receive a newline, - # ## so this may appear to do nothing until you write a newline! - # write_bytes! : List(U8) => {} + ## Write the given bytes to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). + ## + ## Note that many terminals will not actually display content that is written to them until they receive a newline, + ## so this may appear to do nothing until you write a newline! + write_bytes! : List(U8) => Try({}, [StdoutErr(IOErr), ..]) } diff --git a/platform/Utc.roc b/platform/Utc.roc index 18a7b03a..3483d70f 100644 --- a/platform/Utc.roc +++ b/platform/Utc.roc @@ -1,4 +1,23 @@ Utc := [].{ - ## Get the current time as nanoseconds since the Unix epoch (January 1, 1970). + ## Get the current UTC time as nanoseconds since the Unix epoch (January 1, 1970). now! : {} => U128 + + ## Convert nanoseconds since epoch to milliseconds since epoch. + to_millis_since_epoch : U128 -> U128 + to_millis_since_epoch = |nanos| nanos // 1_000_000 + + ## Convert milliseconds since epoch to nanoseconds since epoch. + from_millis_since_epoch : U128 -> U128 + from_millis_since_epoch = |millis| millis * 1_000_000 + + ## Calculate the difference between two timestamps in nanoseconds. + delta_as_nanos : U128, U128 -> U128 + delta_as_nanos = |a, b| if a > b { a - b } else { b - a } + + ## Calculate the difference between two timestamps in milliseconds. + delta_as_millis : U128, U128 -> U128 + delta_as_millis = |a, b| { + nanos = if a > b { a - b } else { b - a } + nanos // 1_000_000 + } } diff --git a/platform/main.roc b/platform/main.roc index 3d8cafee..c088aa39 100644 --- a/platform/main.roc +++ b/platform/main.roc @@ -1,6 +1,6 @@ platform "" requires {} { main! : List(Str) => Try({}, [Exit(I32), ..]) } - exposes [Cmd, Dir, Env, File, Locale, Path, Random, Sleep, Stdin, Stdout, Stderr, Tty, Utc] + exposes [Cmd, Dir, Env, File, IOErr, Locale, Path, Random, Sleep, Stdin, Stdout, Stderr, Tty, Utc] packages {} provides { main_for_host! : "main_for_host" } targets: { @@ -17,6 +17,7 @@ import Cmd import Dir import Env import File +import IOErr import Locale import Path import Random @@ -32,8 +33,8 @@ main_for_host! = |args| match main!(args) { Ok({}) => 0 Err(Exit(code)) => code - Err(other) => { - Stderr.line!("Program exited with error: ${Str.inspect(other)}") - 1 - } + Err(other) => + match Stderr.line!("Program exited with error: ${Str.inspect(other)}") { + _ => 1 + } } diff --git a/src/lib.rs b/src/lib.rs index bd80b92e..0fb8c5a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ use std::ffi::{c_char, c_void}; use std::fs; -use std::io::{self, BufRead, Write}; +use std::io::{self, BufRead, Read, Write}; use std::sync::atomic::{AtomicBool, Ordering}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; @@ -196,30 +196,24 @@ extern "C" fn roc_crashed_fn(roc_crashed: *const RocCrashed, _env: *mut c_void) // Cmd Module Types and Functions // ============================================================================ -/// Type alias for the Cmd error type: [CmdErr(IOErr)] in Roc -type CmdErr = RocSingleTagWrapper; - -/// Type alias for Try(I32, [CmdErr(IOErr)]) - using official RocTry -type TryI32CmdErr = RocTry; - /// Output record: { stderr_utf8_lossy : Str, stdout_utf8 : Str } -/// Memory layout: Both RocStr are 24 bytes, alphabetical: stderr_utf8_lossy, stdout_utf8 +/// Memory layout: Both RocLists are 24 bytes, alphabetical: stderr_utf8_lossy, stdout_utf8 #[repr(C)] -pub struct CmdOutputSuccess { - pub stderr_utf8_lossy: RocStr, // offset 0 (24 bytes) - pub stdout_utf8: RocStr, // offset 24 (24 bytes) +pub struct OutputFromHostSuccess { + pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) + pub stdout_utf8: RocList, // offset 24 (24 bytes) } -/// NonZeroExit error payload: { exit_code : I32, stderr_utf8_lossy : Str, stdout_utf8_lossy : Str } -/// Memory layout: RocStr (24 bytes) > I32 (4 bytes), so: stderr_utf8_lossy, stdout_utf8_lossy, exit_code +/// Output record: { exit_code : I32, stderr_utf8_lossy : List(U8), stdout_utf8_lossy : List(U8) } +/// Memory layout: RocList (24 bytes) > I32 (4 bytes), so: stderr_utf8_lossy, stdout_utf8_lossy, exit_code #[repr(C)] -pub struct NonZeroExitPayload { - pub stderr_utf8_lossy: RocStr, // offset 0 (24 bytes) - pub stdout_utf8_lossy: RocStr, // offset 24 (24 bytes) +pub struct OutputFromHostFailure { + pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) + pub stdout_utf8_lossy: RocList, // offset 24 (24 bytes) pub exit_code: i32, // offset 48 (4 bytes + padding) } -/// Error type for exec_output!: [CmdErr(IOErr), NonZeroExit({ exit_code, stderr, stdout })] +/// Error type for command_exec_output!: [CmdErr(IOErr), NonZeroExit({ exit_code, stderr, stdout })] /// Alphabetically: CmdErr=0, NonZeroExit=1 #[repr(C)] pub union CmdOutputErrPayload { @@ -269,7 +263,7 @@ type TryCmdOutputResult = RocTry; // ============================================================================ /// Hosted function: Cmd.exec_exit_code! (index 0) -/// Takes Command, returns Try(I32, [CmdErr(IOErr)]) +/// Takes Command, returns Try(I32, IOErr) extern "C" fn hosted_cmd_exec_exit_code( ops: *const RocOps, ret_ptr: *mut c_void, @@ -280,13 +274,13 @@ extern "C" fn hosted_cmd_exec_exit_code( let result = roc_command::command_exec_exit_code(cmd, roc_ops); - let try_result: TryI32CmdErr = match result { + let try_result: RocTry = match result { Ok(exit_code) => RocTry::ok(exit_code), - Err(io_err) => RocTry::err(RocSingleTagWrapper::new(io_err)), + Err(io_err) => RocTry::err(io_err), }; unsafe { - std::ptr::write(ret_ptr as *mut TryI32CmdErr, try_result); + std::ptr::write(ret_ptr as *mut RocTry, try_result); } } @@ -448,48 +442,82 @@ extern "C" fn hosted_dir_list(ops: *const RocOps, ret_ptr: *mut c_void, args_ptr } } -/// Hosted function: Env.cwd! (index 5) -/// Takes {}, returns Str +/// Zero-payload single-variant tag union. +/// Used for [CwdUnavailable], [ExePathUnavailable], etc. +/// Layout is just a u8 discriminant (always 0). +#[repr(C)] +pub struct ZeroPayloadTag { + pub discriminant: u8, +} + +impl ZeroPayloadTag { + pub fn new() -> Self { + Self { discriminant: 0 } + } +} + +/// Type alias for [VarNotFound(Str)] = RocSingleTagWrapper +type VarNotFoundErr = RocSingleTagWrapper; +type TryStrVarNotFound = RocTry; +type TryStrCwdUnavailable = RocTry; +type TryStrExePathUnavailable = RocTry; + +/// Hosted function: Env.cwd! +/// Takes {}, returns Try(Str, [CwdUnavailable]) extern "C" fn hosted_env_cwd(ops: *const RocOps, ret_ptr: *mut c_void, _args_ptr: *mut c_void) { let roc_ops = unsafe { &*ops }; - let cwd = std::env::current_dir() - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_default(); - let roc_str = RocStr::from_str(&cwd, roc_ops); + let try_result: TryStrCwdUnavailable = match std::env::current_dir() { + Ok(path) => { + let roc_str = RocStr::from_str(&path.to_string_lossy(), roc_ops); + RocTry::ok(roc_str) + } + Err(_) => RocTry::err(ZeroPayloadTag::new()), + }; unsafe { - *(ret_ptr as *mut RocStr) = roc_str; + std::ptr::write(ret_ptr as *mut TryStrCwdUnavailable, try_result); } } -/// Hosted function: Env.exe_path! (index 6) -/// Takes {}, returns Str +/// Hosted function: Env.exe_path! +/// Takes {}, returns Try(Str, [ExePathUnavailable]) extern "C" fn hosted_env_exe_path( ops: *const RocOps, ret_ptr: *mut c_void, _args_ptr: *mut c_void, ) { let roc_ops = unsafe { &*ops }; - let exe_path = std::env::current_exe() - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_default(); - let roc_str = RocStr::from_str(&exe_path, roc_ops); + let try_result: TryStrExePathUnavailable = match std::env::current_exe() { + Ok(path) => { + let roc_str = RocStr::from_str(&path.to_string_lossy(), roc_ops); + RocTry::ok(roc_str) + } + Err(_) => RocTry::err(ZeroPayloadTag::new()), + }; unsafe { - *(ret_ptr as *mut RocStr) = roc_str; + std::ptr::write(ret_ptr as *mut TryStrExePathUnavailable, try_result); } } -/// Hosted function: Env.var! (index 7) -/// Takes Str, returns Str +/// Hosted function: Env.var! +/// Takes Str, returns Try(Str, [VarNotFound(Str)]) extern "C" fn hosted_env_var(ops: *const RocOps, ret_ptr: *mut c_void, args_ptr: *mut c_void) { let roc_ops = unsafe { &*ops }; let name = unsafe { let args = args_ptr as *const RocStr; - (*args).as_str() + (*args).as_str().to_string() + }; + let try_result: TryStrVarNotFound = match std::env::var(&name) { + Ok(value) => { + let roc_str = RocStr::from_str(&value, roc_ops); + RocTry::ok(roc_str) + } + Err(_) => { + let roc_name = RocStr::from_str(&name, roc_ops); + RocTry::err(RocSingleTagWrapper::new(roc_name)) + } }; - let value = std::env::var(name).unwrap_or_default(); - let roc_str = RocStr::from_str(&value, roc_ops); unsafe { - *(ret_ptr as *mut RocStr) = roc_str; + std::ptr::write(ret_ptr as *mut TryStrVarNotFound, try_result); } } @@ -723,27 +751,33 @@ extern "C" fn hosted_locale_all(ops: *const RocOps, ret_ptr: *mut c_void, _args_ } } +type TryStrNotAvailable = RocTry; + /// Hosted function: Locale.get! -/// Takes {}, returns Str +/// Takes {}, returns Try(Str, [NotAvailable]) #[cfg(target_os = "macos")] extern "C" fn hosted_locale_get(ops: *const RocOps, ret_ptr: *mut c_void, _args_ptr: *mut c_void) { let roc_ops = unsafe { &*ops }; - let locale = locale_from_env().unwrap_or_else(|| "en-US".to_string()); - let roc_str = RocStr::from_str(&locale, roc_ops); + let try_result: TryStrNotAvailable = match locale_from_env() { + Some(locale) => RocTry::ok(RocStr::from_str(&locale, roc_ops)), + None => RocTry::err(ZeroPayloadTag::new()), + }; unsafe { - *(ret_ptr as *mut RocStr) = roc_str; + std::ptr::write(ret_ptr as *mut TryStrNotAvailable, try_result); } } /// Hosted function: Locale.get! -/// Takes {}, returns Str +/// Takes {}, returns Try(Str, [NotAvailable]) #[cfg(not(target_os = "macos"))] extern "C" fn hosted_locale_get(ops: *const RocOps, ret_ptr: *mut c_void, _args_ptr: *mut c_void) { let roc_ops = unsafe { &*ops }; - let locale = sys_locale::get_locale().unwrap_or_else(|| "en-US".to_string()); - let roc_str = RocStr::from_str(&locale, roc_ops); + let try_result: TryStrNotAvailable = match sys_locale::get_locale() { + Some(locale) => RocTry::ok(RocStr::from_str(&locale, roc_ops)), + None => RocTry::err(ZeroPayloadTag::new()), + }; unsafe { - *(ret_ptr as *mut RocStr) = roc_str; + std::ptr::write(ret_ptr as *mut TryStrNotAvailable, try_result); } } @@ -866,88 +900,304 @@ extern "C" fn hosted_sleep_millis( std::thread::sleep(std::time::Duration::from_millis(millis)); } +/// Type alias for the Stdout error type: [StdoutErr(IOErr)] in Roc +type StdoutErr = RocSingleTagWrapper; + +/// Type alias for Try({}, [StdoutErr(IOErr)]) +type TryUnitStdoutErr = RocTry<(), StdoutErr>; + +/// Type alias for the Stderr error type: [StderrErr(IOErr)] in Roc +type StderrErr = RocSingleTagWrapper; + +/// Type alias for Try({}, [StderrErr(IOErr)]) +type TryUnitStderrErr = RocTry<(), StderrErr>; + /// Hosted function: Stderr.line! -/// Takes Str, returns {} +/// Takes Str, returns Try({}, [StderrErr(IOErr)]) extern "C" fn hosted_stderr_line( - _ops: *const RocOps, - _ret_ptr: *mut c_void, + ops: *const RocOps, + ret_ptr: *mut c_void, args_ptr: *mut c_void, ) { - unsafe { - // RocStr passed from Roc - Roc manages its memory, we just read it + let roc_ops = unsafe { &*ops }; + let result = unsafe { let args = args_ptr as *const RocStr; let message = (*args).as_str(); - let _ = writeln!(io::stderr(), "{}", message); - // DO NOT call decref - Roc owns this memory - // ret_ptr is for unit type {}, so we don't need to write anything + writeln!(io::stderr(), "{}", message) + }; + let try_result: TryUnitStderrErr = match result { + Ok(()) => RocTry::ok(()), + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(RocSingleTagWrapper::new(io_err)) + } + }; + unsafe { + std::ptr::write(ret_ptr as *mut TryUnitStderrErr, try_result); } } -/// Hosted function: Stderr.write! (index 17) -/// Takes Str, returns {} +/// Hosted function: Stderr.write! +/// Takes Str, returns Try({}, [StderrErr(IOErr)]) extern "C" fn hosted_stderr_write( - _ops: *const RocOps, - _ret_ptr: *mut c_void, + ops: *const RocOps, + ret_ptr: *mut c_void, args_ptr: *mut c_void, ) { - unsafe { - // RocStr passed from Roc - Roc manages its memory, we just read it + let roc_ops = unsafe { &*ops }; + let result = unsafe { let args = args_ptr as *const RocStr; let message = (*args).as_str(); - let _ = write!(io::stderr(), "{}", message); - let _ = io::stderr().flush(); - // DO NOT call decref - Roc owns this memory + write!(io::stderr(), "{}", message).and_then(|()| io::stderr().flush()) + }; + let try_result: TryUnitStderrErr = match result { + Ok(()) => RocTry::ok(()), + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(RocSingleTagWrapper::new(io_err)) + } + }; + unsafe { + std::ptr::write(ret_ptr as *mut TryUnitStderrErr, try_result); } } -/// Hosted function: Stdin.line! (index 18) -/// Takes {}, returns Str +/// Hosted function: Stderr.write_bytes! +/// Takes List(U8), returns Try({}, [StderrErr(IOErr)]) +extern "C" fn hosted_stderr_write_bytes( + ops: *const RocOps, + ret_ptr: *mut c_void, + args_ptr: *mut c_void, +) { + let roc_ops = unsafe { &*ops }; + let result = unsafe { + let args = args_ptr as *const RocList; + let bytes = (*args).as_slice(); + io::stderr().write_all(bytes).and_then(|()| io::stderr().flush()) + }; + let try_result: TryUnitStderrErr = match result { + Ok(()) => RocTry::ok(()), + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(RocSingleTagWrapper::new(io_err)) + } + }; + unsafe { + std::ptr::write(ret_ptr as *mut TryUnitStderrErr, try_result); + } +} + +/// Error type for Stdin.line!: [EndOfFile, StdinErr(IOErr)] +/// Alphabetically: EndOfFile=0, StdinErr=1 +/// EndOfFile has no payload, StdinErr has IOErr payload. +/// The union must be sized for the largest variant (StdinErr with IOErr). +#[repr(C)] +pub union StdinLineErrPayload { + end_of_file: (), + stdin_err: core::mem::ManuallyDrop, +} + +#[repr(C)] +pub struct StdinLineErr { + payload: StdinLineErrPayload, + discriminant: u8, // EndOfFile=0, StdinErr=1 +} + +impl StdinLineErr { + pub fn end_of_file() -> Self { + Self { + payload: StdinLineErrPayload { end_of_file: () }, + discriminant: 0, + } + } + + pub fn stdin_err(io_err: roc_io_error::IOErr) -> Self { + Self { + payload: StdinLineErrPayload { + stdin_err: core::mem::ManuallyDrop::new(io_err), + }, + discriminant: 1, + } + } +} + +/// Type alias for Try(Str, [EndOfFile, StdinErr(IOErr)]) +type TryStrStdinLineErr = RocTry; + +/// Hosted function: Stdin.line! +/// Takes {}, returns Try(Str, [EndOfFile, StdinErr(IOErr)]) extern "C" fn hosted_stdin_line(ops: *const RocOps, ret_ptr: *mut c_void, _args_ptr: *mut c_void) { + let roc_ops = unsafe { &*ops }; let mut line = String::new(); - let _ = io::stdin().lock().read_line(&mut line); + let result = io::stdin().lock().read_line(&mut line); + + let try_result: TryStrStdinLineErr = match result { + Ok(0) => { + // EOF - no data read + RocTry::err(StdinLineErr::end_of_file()) + } + Ok(_) => { + // Success - trim trailing newline + let roc_str = RocStr::from_str(line.trim_end_matches('\n'), roc_ops); + RocTry::ok(roc_str) + } + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(StdinLineErr::stdin_err(io_err)) + } + }; + + unsafe { + std::ptr::write(ret_ptr as *mut TryStrStdinLineErr, try_result); + } +} + +/// Type alias for [StdinErr(IOErr)] - single variant tag union +type StdinErr = RocSingleTagWrapper; + +/// Type alias for Try(List(U8), [EndOfFile, StdinErr(IOErr)]) - same error type as line! +type TryBytesStdinLineErr = RocTry, StdinLineErr>; - // Create RocStr - ownership transfers to Roc +/// Type alias for Try(List(U8), [StdinErr(IOErr)]) +type TryBytesStdinErr = RocTry, StdinErr>; + +/// Hosted function: Stdin.bytes! +/// Takes {}, returns Try(List(U8), [EndOfFile, StdinErr(IOErr)]) +extern "C" fn hosted_stdin_bytes( + ops: *const RocOps, + ret_ptr: *mut c_void, + _args_ptr: *mut c_void, +) { let roc_ops = unsafe { &*ops }; - // Trim the trailing newline - let roc_str = RocStr::from_str(line.trim_end_matches('\n'), roc_ops); + let mut buf = vec![0u8; 16384]; // 16 KiB buffer + let result = io::stdin().lock().read(&mut buf); + + let try_result: TryBytesStdinLineErr = match result { + Ok(0) => { + // EOF + RocTry::err(StdinLineErr::end_of_file()) + } + Ok(n) => { + buf.truncate(n); + let mut list = RocList::with_capacity(n, roc_ops); + for byte in &buf[..n] { + list.push(*byte, roc_ops); + } + RocTry::ok(list) + } + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(StdinLineErr::stdin_err(io_err)) + } + }; + + unsafe { + std::ptr::write(ret_ptr as *mut TryBytesStdinLineErr, try_result); + } +} + +/// Hosted function: Stdin.read_to_end! +/// Takes {}, returns Try(List(U8), [StdinErr(IOErr)]) +extern "C" fn hosted_stdin_read_to_end( + ops: *const RocOps, + ret_ptr: *mut c_void, + _args_ptr: *mut c_void, +) { + let roc_ops = unsafe { &*ops }; + let mut buf = Vec::new(); + let result = io::stdin().lock().read_to_end(&mut buf); + + let try_result: TryBytesStdinErr = match result { + Ok(_) => { + let mut list = RocList::with_capacity(buf.len(), roc_ops); + for byte in &buf { + list.push(*byte, roc_ops); + } + RocTry::ok(list) + } + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(RocSingleTagWrapper::new(io_err)) + } + }; unsafe { - *(ret_ptr as *mut RocStr) = roc_str; - // DO NOT call decref - ownership transferred to Roc + std::ptr::write(ret_ptr as *mut TryBytesStdinErr, try_result); } } -/// Hosted function: Stdout.line! (index 19) -/// Takes Str, returns {} +/// Hosted function: Stdout.line! +/// Takes Str, returns Try({}, [StdoutErr(IOErr)]) extern "C" fn hosted_stdout_line( - _ops: *const RocOps, - _ret_ptr: *mut c_void, + ops: *const RocOps, + ret_ptr: *mut c_void, args_ptr: *mut c_void, ) { - unsafe { - // RocStr passed from Roc - Roc manages its memory, we just read it + let roc_ops = unsafe { &*ops }; + let result = unsafe { let args = args_ptr as *const RocStr; let message = (*args).as_str(); - let _ = writeln!(io::stdout(), "{}", message); - // DO NOT call decref - Roc owns this memory - // ret_ptr is for unit type {}, so we don't need to write anything + writeln!(io::stdout(), "{}", message) + }; + let try_result: TryUnitStdoutErr = match result { + Ok(()) => RocTry::ok(()), + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(RocSingleTagWrapper::new(io_err)) + } + }; + unsafe { + std::ptr::write(ret_ptr as *mut TryUnitStdoutErr, try_result); } } /// Hosted function: Stdout.write! -/// Takes Str, returns {} +/// Takes Str, returns Try({}, [StdoutErr(IOErr)]) extern "C" fn hosted_stdout_write( - _ops: *const RocOps, - _ret_ptr: *mut c_void, + ops: *const RocOps, + ret_ptr: *mut c_void, args_ptr: *mut c_void, ) { - unsafe { - // RocStr passed from Roc - Roc manages its memory, we just read it + let roc_ops = unsafe { &*ops }; + let result = unsafe { let args = args_ptr as *const RocStr; let message = (*args).as_str(); - let _ = write!(io::stdout(), "{}", message); - let _ = io::stdout().flush(); - // DO NOT call decref - Roc owns this memory + write!(io::stdout(), "{}", message).and_then(|()| io::stdout().flush()) + }; + let try_result: TryUnitStdoutErr = match result { + Ok(()) => RocTry::ok(()), + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(RocSingleTagWrapper::new(io_err)) + } + }; + unsafe { + std::ptr::write(ret_ptr as *mut TryUnitStdoutErr, try_result); + } +} + +/// Hosted function: Stdout.write_bytes! +/// Takes List(U8), returns Try({}, [StdoutErr(IOErr)]) +extern "C" fn hosted_stdout_write_bytes( + ops: *const RocOps, + ret_ptr: *mut c_void, + args_ptr: *mut c_void, +) { + let roc_ops = unsafe { &*ops }; + let result = unsafe { + let args = args_ptr as *const RocList; + let bytes = (*args).as_slice(); + io::stdout().write_all(bytes).and_then(|()| io::stdout().flush()) + }; + let try_result: TryUnitStdoutErr = match result { + Ok(()) => RocTry::ok(()), + Err(e) => { + let io_err = roc_io_error::IOErr::from_io_error(&e, roc_ops); + RocTry::err(RocSingleTagWrapper::new(io_err)) + } + }; + unsafe { + std::ptr::write(ret_ptr as *mut TryUnitStdoutErr, try_result); } } @@ -988,38 +1238,42 @@ extern "C" fn hosted_utc_now(_ops: *const RocOps, ret_ptr: *mut c_void, _args_pt /// Array of hosted function pointers, sorted alphabetically by fully-qualified name. /// IMPORTANT: Order must match the order Roc expects based on alphabetical sorting. -static HOSTED_FNS: [HostedFn; 31] = [ - hosted_cmd_exec_exit_code, // 0: Cmd.exec_exit_code! - hosted_cmd_exec_output, // 1: Cmd.exec_output! - hosted_dir_create, // 2: Dir.create! - hosted_dir_create_all, // 3: Dir.create_all! - hosted_dir_delete_all, // 4: Dir.delete_all! - hosted_dir_delete_empty, // 5: Dir.delete_empty! - hosted_dir_list, // 6: Dir.list! - hosted_env_cwd, // 7: Env.cwd! - hosted_env_exe_path, // 8: Env.exe_path! - hosted_env_var, // 9: Env.var! - hosted_file_delete, // 10: File.delete! - hosted_file_read_bytes, // 11: File.read_bytes! - hosted_file_read_utf8, // 12: File.read_utf8! - hosted_file_write_bytes, // 13: File.write_bytes! - hosted_file_write_utf8, // 14: File.write_utf8! - hosted_locale_all, // 15: Locale.all! - hosted_locale_get, // 16: Locale.get! - hosted_path_is_dir, // 17: Path.is_dir! - hosted_path_is_file, // 18: Path.is_file! - hosted_path_is_sym_link, // 19: Path.is_sym_link! - hosted_random_seed_u32, // 20: Random.seed_u32! - hosted_random_seed_u64, // 21: Random.seed_u64! - hosted_sleep_millis, // 22: Sleep.millis! - hosted_stderr_line, // 23: Stderr.line! - hosted_stderr_write, // 24: Stderr.write! - hosted_stdin_line, // 25: Stdin.line! - hosted_stdout_line, // 26: Stdout.line! - hosted_stdout_write, // 27: Stdout.write! - hosted_tty_disable_raw_mode, // 28: Tty.disable_raw_mode! - hosted_tty_enable_raw_mode, // 29: Tty.enable_raw_mode! - hosted_utc_now, // 30: Utc.now! +static HOSTED_FNS: [HostedFn; 35] = [ + hosted_cmd_exec_exit_code, // 0: Cmd.exec_exit_code! + hosted_cmd_exec_output, // 1: Cmd.exec_output! + hosted_dir_create, // 2: Dir.create! + hosted_dir_create_all, // 3: Dir.create_all! + hosted_dir_delete_all, // 4: Dir.delete_all! + hosted_dir_delete_empty, // 5: Dir.delete_empty! + hosted_dir_list, // 6: Dir.list! + hosted_env_cwd, // 7: Env.cwd! + hosted_env_exe_path, // 8: Env.exe_path! + hosted_env_var, // 9: Env.var! + hosted_file_delete, // 10: File.delete! + hosted_file_read_bytes, // 11: File.read_bytes! + hosted_file_read_utf8, // 12: File.read_utf8! + hosted_file_write_bytes, // 13: File.write_bytes! + hosted_file_write_utf8, // 14: File.write_utf8! + hosted_locale_all, // 15: Locale.all! + hosted_locale_get, // 16: Locale.get! + hosted_path_is_dir, // 17: Path.is_dir! + hosted_path_is_file, // 18: Path.is_file! + hosted_path_is_sym_link, // 19: Path.is_sym_link! + hosted_random_seed_u32, // 20: Random.seed_u32! + hosted_random_seed_u64, // 21: Random.seed_u64! + hosted_sleep_millis, // 22: Sleep.millis! + hosted_stderr_line, // 23: Stderr.line! + hosted_stderr_write, // 24: Stderr.write! + hosted_stderr_write_bytes, // 25: Stderr.write_bytes! + hosted_stdin_bytes, // 26: Stdin.bytes! + hosted_stdin_line, // 27: Stdin.line! + hosted_stdin_read_to_end, // 28: Stdin.read_to_end! + hosted_stdout_line, // 29: Stdout.line! + hosted_stdout_write, // 30: Stdout.write! + hosted_stdout_write_bytes, // 31: Stdout.write_bytes! + hosted_tty_disable_raw_mode, // 32: Tty.disable_raw_mode! + hosted_tty_enable_raw_mode, // 33: Tty.enable_raw_mode! + hosted_utc_now, // 34: Utc.now! ]; /// Build a RocList from command-line arguments. From 2edffe4e5a2461c0618bed280d4001178accfaf0 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:28:53 +0100 Subject: [PATCH 03/11] get rid of CmdInternal --- crates/roc_command/src/lib.rs | 121 ++++++++++++-------- examples/command.roc | 24 ++-- platform/Cmd.roc | 203 +++++++++++++++++++--------------- platform/CmdInternal.roc | 25 ++--- src/lib.rs | 176 ++++++++++++++--------------- 5 files changed, 289 insertions(+), 260 deletions(-) diff --git a/crates/roc_command/src/lib.rs b/crates/roc_command/src/lib.rs index f6e99704..4975ceb4 100644 --- a/crates/roc_command/src/lib.rs +++ b/crates/roc_command/src/lib.rs @@ -33,6 +33,45 @@ impl RocRefcounted for Command { } } +impl std::fmt::Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let args_str = self + .args + .iter() + .map(|a| a.as_str()) + .collect::>() + .join(" "); + + let envs_slice = self.envs.as_slice(); + let envs_str = envs_slice + .chunks(2) + .filter(|c| c.len() == 2) + .map(|c| format!("{}={}", c[0].as_str(), c[1].as_str())) + .collect::>() + .join(" "); + let envs_part = if envs_str.is_empty() { + String::new() + } else { + format!(", envs: {envs_str}") + }; + + let clear_envs_part = if self.clear_envs != 0 { + ", clear_envs: true" + } else { + "" + }; + + write!( + f, + "{{ cmd: {}, args: {}{}{} }}", + self.program.as_str(), + args_str, + envs_part, + clear_envs_part, + ) + } +} + impl Command { /// Convert to std::process::Command pub fn to_std_command(&self) -> std::process::Command { @@ -86,26 +125,22 @@ impl RocRefcounted for CommandOutputSuccess { } } -/// Output when command fails (non-zero exit code) -/// Roc type: { exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str } +/// Represents the record inside the Roc tag `FailedToGetExitCode({ command : Str, err : IOErr })` /// Memory layout: Fields sorted by size descending, then alphabetically. -/// RocStr (24 bytes) > I32 (4 bytes), so: stderr_utf8_lossy (24), stdout_utf8_lossy (24), exit_code (4) +/// RocStr (24 bytes) > IOErr (??? bytes) #[derive(Clone, Debug)] #[repr(C)] -pub struct CommandOutputFailure { - pub stderr_utf8_lossy: RocStr, // offset 0 (24 bytes) - pub stdout_utf8_lossy: RocStr, // offset 24 (24 bytes) - pub exit_code: i32, // offset 48 (4 bytes + padding) +pub struct FailedToGetExitCodeContent { + pub command: RocStr, // offset 0 (24 bytes) + pub err: roc_io_error::IOErr, // offset 24 (??? bytes) } -impl RocRefcounted for CommandOutputFailure { +impl RocRefcounted for FailedToGetExitCodeContent { fn inc(&mut self) { - self.stderr_utf8_lossy.inc(); - self.stdout_utf8_lossy.inc(); + self.command.inc(); } fn dec(&mut self) { - self.stderr_utf8_lossy.dec(); - self.stdout_utf8_lossy.dec(); + self.command.dec(); } fn is_refcounted() -> bool { true @@ -119,16 +154,6 @@ fn bytes_to_roc_str_lossy(bytes: &[u8], roc_ops: &RocOps) -> RocStr { RocStr::from_str(s.as_ref(), roc_ops) } -/// Result of executing a command for output -pub enum CommandOutputResult { - /// Command succeeded with exit code 0 - Success(CommandOutputSuccess), - /// Command failed with non-zero exit code - NonZeroExit(CommandOutputFailure), - /// Command failed to execute - Error(IOErr), -} - /// Execute command and return exit code pub fn command_exec_exit_code(cmd: &Command, roc_ops: &RocOps) -> Result { match cmd.to_std_command().status() { @@ -140,29 +165,29 @@ pub fn command_exec_exit_code(cmd: &Command, roc_ops: &RocOps) -> Result CommandOutputResult { - match cmd.to_std_command().output() { - Ok(output) => { - let stdout_utf8 = bytes_to_roc_str_lossy(&output.stdout, roc_ops); - let stderr_utf8_lossy = bytes_to_roc_str_lossy(&output.stderr, roc_ops); - - match output.status.code() { - Some(0) => CommandOutputResult::Success(CommandOutputSuccess { - stderr_utf8_lossy, - stdout_utf8, - }), - Some(exit_code) => CommandOutputResult::NonZeroExit(CommandOutputFailure { - stderr_utf8_lossy, - stdout_utf8_lossy: stdout_utf8, - exit_code, - }), - None => CommandOutputResult::Error( - IOErr::new_other("Process was killed by signal", roc_ops) - ), - } - } - Err(e) => CommandOutputResult::Error(IOErr::from_io_error(&e, roc_ops)), - } -} +// /// Execute command and capture stdout/stderr as UTF-8 strings. +// /// Invalid UTF-8 sequences are replaced with the Unicode replacement character. +// pub fn command_exec_output(cmd: &Command, roc_ops: &RocOps) -> CommandOutputResult { +// match cmd.to_std_command().output() { +// Ok(output) => { +// let stdout_utf8 = bytes_to_roc_str_lossy(&output.stdout, roc_ops); +// let stderr_utf8_lossy = bytes_to_roc_str_lossy(&output.stderr, roc_ops); + +// match output.status.code() { +// Some(0) => CommandOutputResult::Success(CommandOutputSuccess { +// stderr_utf8_lossy, +// stdout_utf8, +// }), +// Some(exit_code) => CommandOutputResult::NonZeroExit(CommandOutputFailure { +// stderr_utf8_lossy, +// stdout_utf8_lossy: stdout_utf8, +// exit_code, +// }), +// None => CommandOutputResult::Error( +// IOErr::new_other("Process was killed by signal", roc_ops) +// ), +// } +// } +// Err(e) => CommandOutputResult::Error(IOErr::from_io_error(&e, roc_ops)), +// } +// } diff --git a/examples/command.roc b/examples/command.roc index 025e0513..fd726ef7 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -7,23 +7,23 @@ import pf.Cmd main! = |_args| { # Simplest way to execute a command (prints to your terminal). - Cmd.exec!("echo", ["Hello"])? + #Cmd.exec!("echo", ["Hello"])? # To execute and capture the output (stdout and stderr) without inheriting your terminal. - cmd_output = - Cmd.new("echo") - .args(["Hi"]) - .exec_output!()? + #cmd_output = + # Cmd.new("echo") + # .args(["Hi"]) + # .exec_output!()? - Stdout.line!("${Str.inspect(cmd_output)}")? + #Stdout.line!("${Str.inspect(cmd_output)}")? # To run a command with environment variables. - Cmd.new("env") - .clear_envs() # You probably don't need to clear all other environment variables, this is just an example. - .env("FOO", "BAR") - .envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs` - .args(["-v"]) - .exec_cmd!()? + #Cmd.new("env") + # .clear_envs() # You probably don't need to clear all other environment variables, this is just an example. + # .env("FOO", "BAR") + # .envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs` + # .args(["-v"]) + # .exec_cmd!()? # To execute and just get the exit code (prints to your terminal). # Prefer using `exec!` or `exec_cmd!`. diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 3da42978..0cc3dec4 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -1,10 +1,9 @@ import IOErr exposing [IOErr] -import CmdInternal Cmd :: { args : List(Str), clear_envs : Bool, - envs : List(Str), + envs : List(Str), # TODO change this to List((Str, Str)) program : Str, }.{ @@ -16,20 +15,20 @@ Cmd :: { ## ```roc ## Cmd.exec!("echo", ["hello world"])? ## ``` - exec! : Str, List(Str) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode { command : Str, err : IOErr }, ..]) - exec! = |program, arguments| { - exit_code = - new(program) - .args(arguments) - .exec_exit_code!()? - - if exit_code == 0 { - Ok({}) - } else { - command = "${cmd_name} ${arguments.join_with(" ")}" - Err(ExecFailed({ command, exit_code })) - } - } + #exec! : Str, List(Str) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode { command : Str, err : IOErr }, ..]) + #exec! = |program, arguments| { + # exit_code = + # new(program) + # .args(arguments) + # .exec_exit_code!()? + + # if exit_code == 0 { + # Ok({}) + # } else { + # command = "${cmd_name} ${arguments.join_with(" ")}" + # Err(ExecFailed({ command, exit_code })) + # } + #} ## Execute a Cmd (using the builder pattern). ## Stdin, stdout, and stderr are inherited from the parent process. @@ -43,16 +42,16 @@ Cmd :: { ## .env("RUST_BACKTRACE", "1") ## .exec_cmd!()? ## ``` - exec_cmd! : Cmd => Try({}, [ExecCmdFailed { command : Str, exit_code : I32 }, FailedToGetExitCode { command : Str, err : IOErr }, ..]) - exec_cmd! = |cmd| { - exit_code = exec_exit_code!(cmd)? - - if exit_code == 0 { - Ok({}) - } else { - Err(ExecCmdFailed({ command: to_str(cmd), exit_code })) - } - } + #exec_cmd! : Cmd => Try({}, [ExecCmdFailed { command : Str, exit_code : I32 }, FailedToGetExitCode { command : Str, err : IOErr }, ..]) + #exec_cmd! = |cmd| { + # exit_code = exec_exit_code!(cmd)? + # + # if exit_code == 0 { + # Ok({}) + # } else { + # Err(ExecCmdFailed({ command: to_str(cmd), exit_code })) + # } + #} ## Execute command and capture stdout and stderr as UTF-8 strings. ## Invalid UTF-8 sequences are replaced with the Unicode replacement character. @@ -68,40 +67,40 @@ Cmd :: { ## ## Stdout.line!("Echo output: ${cmd_output.stdout_utf8}")? ## ``` - exec_output! : Cmd => Try( - { stdout_utf8 : Str, stderr_utf8_lossy : Str }, - [ - StdoutContainsInvalidUtf8({ cmd_str : Str, err : [BadUtf8 { index : U64, problem : Str.Utf8Problem }] }), - NonZeroExitCode({ command : Str, exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str }), - FailedToGetExitCode({ command : Str, err : IOErr }), - .. - ] - ) - exec_output! = |cmd| - exec_try = CmdInternal.command_exec_output!(cmd) - - match exec_try { - Ok({ stderr_bytes, stdout_bytes }) => - stdout_utf8 = - Str.from_utf8(stdout_bytes) - .map_err(|err| StdoutContainsInvalidUtf8({ cmd_str: to_str(cmd), err }))? - - stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) - - Ok({ stdout_utf8, stderr_utf8_lossy }) - - Err(inside_try) => - match inside_try { - Ok({ exit_code, stderr_bytes, stdout_bytes }) => - stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes) - stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) - - Err(NonZeroExitCode({ command: to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy })) - - Err(err) => - Err(FailedToGetExitCode({ command: to_str(cmd), err: InternalIOErr.handle_err(err) })) - } - } + #exec_output! : Cmd => Try( + # { stdout_utf8 : Str, stderr_utf8_lossy : Str }, + # [ + # StdoutContainsInvalidUtf8({ cmd_str : Str, err : [BadUtf8 { index : U64, problem : Str.Utf8Problem }] }), + # NonZeroExitCode({ command : Str, exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str }), + # FailedToGetExitCode({ command : Str, err : IOErr }), + # .. + # ] + #) + #exec_output! = |cmd| + # exec_try = CmdInternal.command_exec_output!(cmd) + + # match exec_try { + # Ok({ stderr_bytes, stdout_bytes }) => + # stdout_utf8 = + # Str.from_utf8(stdout_bytes) + # .map_err(|err| StdoutContainsInvalidUtf8({ cmd_str: to_str(cmd), err }))? + + # stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) + + # Ok({ stdout_utf8, stderr_utf8_lossy }) + + # Err(inside_try) => + # match inside_try { + # Ok({ exit_code, stderr_bytes, stdout_bytes }) => + # stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes) + # stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) + + # Err(NonZeroExitCode({ command: to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy })) + + # Err(err) => + # Err(FailedToGetExitCode({ command: to_str(cmd), err: InternalIOErr.handle_err(err) })) + # } + # } ## Execute command and capture stdout and stderr in the original form as bytes. ## @@ -115,31 +114,31 @@ Cmd :: { ## ## Stdout.line!("${Str.inspect(cmd_output_bytes)}")? # {stderr_bytes: [], stdout_bytes: [72, 105, 10]} ## ``` - exec_output_bytes! : Cmd => Try( - { stderr_bytes : List(U8), stdout_bytes : List(U8) } - [ - FailedToGetExitCodeB(IOErr), # TODO: perhaps no need for B? - NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) }), - .. - ] - ) - exec_output_bytes! = |cmd| { - exec_try = CmdInternal.command_exec_output!(cmd) # TODO - - match exec_try { - Ok({ stderr_bytes, stdout_bytes }) => - Ok({ stdout_bytes, stderr_bytes }) - - Err(inside_try) => - match inside_try { - Ok({ exit_code, stderr_bytes, stdout_bytes }) -> - Err(NonZeroExitCodeB({ exit_code, stdout_bytes, stderr_bytes })) - - Err(err) -> - Err(FailedToGetExitCodeB(InternalIOErr.handle_err(err))) - } - } - } + #exec_output_bytes! : Cmd => Try( + # { stderr_bytes : List(U8), stdout_bytes : List(U8) } + # [ + # FailedToGetExitCodeB(IOErr), # TODO: perhaps no need for B? + # NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) }), + # .. + # ] + #) + #exec_output_bytes! = |cmd| { + # exec_try = CmdInternal.command_exec_output!(cmd) # TODO + + # match exec_try { + # Ok({ stderr_bytes, stdout_bytes }) => + # Ok({ stdout_bytes, stderr_bytes }) + + # Err(inside_try) => + # match inside_try { + # Ok({ exit_code, stderr_bytes, stdout_bytes }) -> + # Err(NonZeroExitCodeB({ exit_code, stdout_bytes, stderr_bytes })) + + # Err(err) -> + # Err(FailedToGetExitCodeB(InternalIOErr.handle_err(err))) + # } + # } + #} ## Execute a command and return its exit code. ## Stdin, stdout, and stderr are inherited from the parent process. @@ -151,11 +150,7 @@ Cmd :: { ## ```roc ## exit_code = Cmd.new("cat").arg("non_existent.txt").exec_exit_code!()? ## ``` - exec_exit_code! : Cmd => Try(I32, [FailedToGetExitCode { command : Str, err : IOErr }, ..]) - exec_exit_code! = |cmd| { - CmdInternal.command_exec_exit_code!(cmd) # TODO - .map_err(|err| FailedToGetExitCode({ command: to_str(cmd), err })) - } + exec_exit_code! : Cmd => Try(I32, [FailedToGetExitCode({ command : Str, err : IOErr }), ..]) ## Create a new command with the given program name. Use a function that starts with `exec_` to execute it. ## @@ -237,4 +232,32 @@ Cmd :: { envs: cmd.envs, program: cmd.program, } + + to_str : Cmd -> Str + to_str = |cmd| { + my_trim = |trimmed_str| {if trimmed_str.is_empty() "" else "envs: ${trimmed_str}"} + + envs_str = + cmd.envs + # TODO once we're using List of tuples: .map(|(key, value)| "${key}=${value}") + .join_with(" ") + .trim()->my_trim() + + clear_envs_str = if cmd.clear_envs ", clear_envs: true" else "" + + \\{ cmd: ${cmd.program}, args: ${Str.join_with(cmd.args, " ")}${envs_str}${clear_envs_str} } + } +} + +# Do not change the order of the fields! It will lead to a segfault. +OutputFromHostSuccess : { + stderr_bytes : List(U8), + stdout_bytes : List(U8), +} + +# Do not change the order of the fields! It will lead to a segfault. +OutputFromHostFailure : { + stderr_bytes : List(U8), + stdout_bytes : List(U8), + exit_code : I32, } diff --git a/platform/CmdInternal.roc b/platform/CmdInternal.roc index 91ea2283..0778867a 100644 --- a/platform/CmdInternal.roc +++ b/platform/CmdInternal.roc @@ -1,21 +1,12 @@ -import Cmd exposing [Cmd] import IOErr exposing [IOErr] -CmdInternal :: [].{ - command_exec_exit_code! : Cmd => Try(I32, IOErr) +CmdInternal :: { + args : List(Str), + clear_envs : Bool, + envs : List(Str), + program : Str, +}.{ + command_exec_exit_code! : CmdInternal => Try(I32, IOErr) - command_exec_output! : Cmd => Try(OutputFromHostSuccess, (Try(OutputFromHostFailure, IOErr))) -} - -# Do not change the order of the fields! It will lead to a segfault. -OutputFromHostSuccess : { - stderr_bytes : List(U8), - stdout_bytes : List(U8), -} - -# Do not change the order of the fields! It will lead to a segfault. -OutputFromHostFailure : { - stderr_bytes : List(U8), - stdout_bytes : List(U8), - exit_code : I32, + #command_exec_output! : CmdInternal => Try(OutputFromHostSuccess, (Try(OutputFromHostFailure, IOErr))) } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 0fb8c5a6..0f766605 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use std::io::{self, BufRead, Read, Write}; use std::sync::atomic::{AtomicBool, Ordering}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use roc_command::FailedToGetExitCodeContent; use roc_std_new::{ HostedFn, HostedFunctions, RocAlloc, RocCrashed, RocDbg, RocDealloc, RocExpectFailed, RocList, RocOps, RocRealloc, RocStr, RocTry, @@ -198,65 +199,65 @@ extern "C" fn roc_crashed_fn(roc_crashed: *const RocCrashed, _env: *mut c_void) /// Output record: { stderr_utf8_lossy : Str, stdout_utf8 : Str } /// Memory layout: Both RocLists are 24 bytes, alphabetical: stderr_utf8_lossy, stdout_utf8 -#[repr(C)] -pub struct OutputFromHostSuccess { - pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) - pub stdout_utf8: RocList, // offset 24 (24 bytes) -} - -/// Output record: { exit_code : I32, stderr_utf8_lossy : List(U8), stdout_utf8_lossy : List(U8) } -/// Memory layout: RocList (24 bytes) > I32 (4 bytes), so: stderr_utf8_lossy, stdout_utf8_lossy, exit_code -#[repr(C)] -pub struct OutputFromHostFailure { - pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) - pub stdout_utf8_lossy: RocList, // offset 24 (24 bytes) - pub exit_code: i32, // offset 48 (4 bytes + padding) -} - -/// Error type for command_exec_output!: [CmdErr(IOErr), NonZeroExit({ exit_code, stderr, stdout })] -/// Alphabetically: CmdErr=0, NonZeroExit=1 -#[repr(C)] -pub union CmdOutputErrPayload { - cmd_err: core::mem::ManuallyDrop, - non_zero_exit: core::mem::ManuallyDrop, -} - -#[repr(C)] -pub struct CmdOutputErr { - payload: CmdOutputErrPayload, - discriminant: u8, // CmdErr=0, NonZeroExit=1 -} - -impl CmdOutputErr { - pub fn cmd_err(io_err: roc_io_error::IOErr) -> Self { - Self { - payload: CmdOutputErrPayload { - cmd_err: core::mem::ManuallyDrop::new(io_err), - }, - discriminant: 0, - } - } - - pub fn non_zero_exit( - stderr_utf8_lossy: RocStr, - stdout_utf8_lossy: RocStr, - exit_code: i32, - ) -> Self { - Self { - payload: CmdOutputErrPayload { - non_zero_exit: core::mem::ManuallyDrop::new(NonZeroExitPayload { - stderr_utf8_lossy, - stdout_utf8_lossy, - exit_code, - }), - }, - discriminant: 1, - } - } -} +// #[repr(C)] +// pub struct OutputFromHostSuccess { +// pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) +// pub stdout_utf8: RocList, // offset 24 (24 bytes) +// } + +// /// Output record: { exit_code : I32, stderr_utf8_lossy : List(U8), stdout_utf8_lossy : List(U8) } +// /// Memory layout: RocList (24 bytes) > I32 (4 bytes), so: stderr_utf8_lossy, stdout_utf8_lossy, exit_code +// #[repr(C)] +// pub struct OutputFromHostFailure { +// pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) +// pub stdout_utf8_lossy: RocList, // offset 24 (24 bytes) +// pub exit_code: i32, // offset 48 (4 bytes + padding) +// } + +// /// Error type for command_exec_output!: [CmdErr(IOErr), NonZeroExit({ exit_code, stderr, stdout })] +// /// Alphabetically: CmdErr=0, NonZeroExit=1 +// #[repr(C)] +// pub union CmdOutputErrPayload { +// cmd_err: core::mem::ManuallyDrop, +// non_zero_exit: core::mem::ManuallyDrop, +// } + +// #[repr(C)] +// pub struct CmdOutputErr { +// payload: CmdOutputErrPayload, +// discriminant: u8, // CmdErr=0, NonZeroExit=1 +// } + +// impl CmdOutputErr { +// pub fn cmd_err(io_err: roc_io_error::IOErr) -> Self { +// Self { +// payload: CmdOutputErrPayload { +// cmd_err: core::mem::ManuallyDrop::new(io_err), +// }, +// discriminant: 0, +// } +// } + +// pub fn non_zero_exit( +// stderr_utf8_lossy: RocStr, +// stdout_utf8_lossy: RocStr, +// exit_code: i32, +// ) -> Self { +// Self { +// payload: CmdOutputErrPayload { +// non_zero_exit: core::mem::ManuallyDrop::new(NonZeroExitPayload { +// stderr_utf8_lossy, +// stdout_utf8_lossy, +// exit_code, +// }), +// }, +// discriminant: 1, +// } +// } +// } /// Type alias for Try({ stderr, stdout }, [CmdErr(IOErr), NonZeroExit(...)]) - using official RocTry -type TryCmdOutputResult = RocTry; +//type TryCmdOutputResult = RocTry; // ============================================================================ // Hosted Functions (sorted alphabetically by fully-qualified name) @@ -272,50 +273,39 @@ extern "C" fn hosted_cmd_exec_exit_code( let roc_ops = unsafe { &*ops }; let cmd = unsafe { &*(args_ptr as *const roc_command::Command) }; - let result = roc_command::command_exec_exit_code(cmd, roc_ops); - - let try_result: RocTry = match result { - Ok(exit_code) => RocTry::ok(exit_code), - Err(io_err) => RocTry::err(io_err), + let exec_try = match roc_command::command_exec_exit_code(cmd, roc_ops) { + Ok(code) => RocTry::ok(code), + Err(io_err) => { + let cmd_as_str = RocStr::from_str(&cmd.to_string(), roc_ops); + RocTry::err( + RocSingleTagWrapper::new( + roc_command::FailedToGetExitCodeContent{command: cmd_as_str, err: io_err} + ) + ) + }, }; unsafe { - std::ptr::write(ret_ptr as *mut RocTry, try_result); + std::ptr::write(ret_ptr as *mut RocTry>, exec_try); } } /// Hosted function: Cmd.exec_output! (index 1) /// Takes Command, returns Try({ stderr_utf8_lossy, stdout_utf8 }, [CmdErr(IOErr), NonZeroExit(...)]) -extern "C" fn hosted_cmd_exec_output( - ops: *const RocOps, - ret_ptr: *mut c_void, - args_ptr: *mut c_void, -) { - let roc_ops = unsafe { &*ops }; - let cmd = unsafe { &*(args_ptr as *const roc_command::Command) }; +// extern "C" fn hosted_cmd_exec_output( +// ops: *const RocOps, +// ret_ptr: *mut c_void, +// args_ptr: *mut c_void, +// ) { +// let roc_ops = unsafe { &*ops }; +// let cmd = unsafe { &*(args_ptr as *const roc_command::Command) }; - let result = roc_command::command_exec_output(cmd, roc_ops); - let try_result: TryCmdOutputResult = match result { - roc_command::CommandOutputResult::Success(output) => RocTry::ok(CmdOutputSuccess { - stderr_utf8_lossy: output.stderr_utf8_lossy, - stdout_utf8: output.stdout_utf8, - }), - roc_command::CommandOutputResult::NonZeroExit(failure) => { - RocTry::err(CmdOutputErr::non_zero_exit( - failure.stderr_utf8_lossy, - failure.stdout_utf8_lossy, - failure.exit_code, - )) - } - roc_command::CommandOutputResult::Error(io_err) => { - RocTry::err(CmdOutputErr::cmd_err(io_err)) - } - }; +// let result = roc_command::command_exec_output(cmd, roc_ops); - unsafe { - std::ptr::write(ret_ptr as *mut TryCmdOutputResult, try_result); - } -} +// unsafe { +// std::ptr::write(ret_ptr as *mut TryCmdOutputResult, result); +// } +// } /// Hosted function: Dir.create! (index 2) /// Takes Str, returns Try({}, [DirErr(IOErr)]) @@ -1238,9 +1228,9 @@ extern "C" fn hosted_utc_now(_ops: *const RocOps, ret_ptr: *mut c_void, _args_pt /// Array of hosted function pointers, sorted alphabetically by fully-qualified name. /// IMPORTANT: Order must match the order Roc expects based on alphabetical sorting. -static HOSTED_FNS: [HostedFn; 35] = [ +static HOSTED_FNS: [HostedFn; 34] = [ hosted_cmd_exec_exit_code, // 0: Cmd.exec_exit_code! - hosted_cmd_exec_output, // 1: Cmd.exec_output! + //hosted_cmd_exec_output, // 1: Cmd.exec_output! hosted_dir_create, // 2: Dir.create! hosted_dir_create_all, // 3: Dir.create_all! hosted_dir_delete_all, // 4: Dir.delete_all! From dcab0b9b64dbfc3bac380625cb2c7ce073cb4c73 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:53:33 +0100 Subject: [PATCH 04/11] WIP Cmd refactor --- crates/roc_command/src/lib.rs | 68 ----------------------------------- examples/command.roc | 18 +++++++--- platform/Cmd.roc | 32 ++++++++++------- platform/CmdInternal.roc | 12 ------- src/lib.rs | 16 +++------ 5 files changed, 38 insertions(+), 108 deletions(-) delete mode 100644 platform/CmdInternal.roc diff --git a/crates/roc_command/src/lib.rs b/crates/roc_command/src/lib.rs index 4975ceb4..a1c5ba99 100644 --- a/crates/roc_command/src/lib.rs +++ b/crates/roc_command/src/lib.rs @@ -33,45 +33,6 @@ impl RocRefcounted for Command { } } -impl std::fmt::Display for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let args_str = self - .args - .iter() - .map(|a| a.as_str()) - .collect::>() - .join(" "); - - let envs_slice = self.envs.as_slice(); - let envs_str = envs_slice - .chunks(2) - .filter(|c| c.len() == 2) - .map(|c| format!("{}={}", c[0].as_str(), c[1].as_str())) - .collect::>() - .join(" "); - let envs_part = if envs_str.is_empty() { - String::new() - } else { - format!(", envs: {envs_str}") - }; - - let clear_envs_part = if self.clear_envs != 0 { - ", clear_envs: true" - } else { - "" - }; - - write!( - f, - "{{ cmd: {}, args: {}{}{} }}", - self.program.as_str(), - args_str, - envs_part, - clear_envs_part, - ) - } -} - impl Command { /// Convert to std::process::Command pub fn to_std_command(&self) -> std::process::Command { @@ -125,35 +86,6 @@ impl RocRefcounted for CommandOutputSuccess { } } -/// Represents the record inside the Roc tag `FailedToGetExitCode({ command : Str, err : IOErr })` -/// Memory layout: Fields sorted by size descending, then alphabetically. -/// RocStr (24 bytes) > IOErr (??? bytes) -#[derive(Clone, Debug)] -#[repr(C)] -pub struct FailedToGetExitCodeContent { - pub command: RocStr, // offset 0 (24 bytes) - pub err: roc_io_error::IOErr, // offset 24 (??? bytes) -} - -impl RocRefcounted for FailedToGetExitCodeContent { - fn inc(&mut self) { - self.command.inc(); - } - fn dec(&mut self) { - self.command.dec(); - } - fn is_refcounted() -> bool { - true - } -} - -/// Convert bytes to RocStr using lossy UTF-8 conversion. -/// Invalid UTF-8 sequences are replaced with the Unicode replacement character (U+FFFD). -fn bytes_to_roc_str_lossy(bytes: &[u8], roc_ops: &RocOps) -> RocStr { - let s = String::from_utf8_lossy(bytes); - RocStr::from_str(s.as_ref(), roc_ops) -} - /// Execute command and return exit code pub fn command_exec_exit_code(cmd: &Command, roc_ops: &RocOps) -> Result { match cmd.to_std_command().status() { diff --git a/examples/command.roc b/examples/command.roc index fd726ef7..d673b640 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -27,12 +27,22 @@ main! = |_args| { # To execute and just get the exit code (prints to your terminal). # Prefer using `exec!` or `exec_cmd!`. - exit_code = - Cmd.new("cat") + output1 = + Cmd.new("cattt") # line! prints do work if this is just `cat` (this is the non-error case) .args(["non_existent.txt"]) - .exec_exit_code!()? + .exec_exit_code!() - Stdout.line!("Exit code: ${exit_code.to_str()}")? + Stdout.line!("pre Inspect1")? + + Stdout.line!("Inspect1: ${Str.inspect(output1)}")? + + Stdout.line!("post Inspect1")? + + output2 = output1? + + Stdout.line!("Inspect2: ${Str.inspect(output2)}")? + + Stdout.line!("done")? # TODO add exec_output_bytes diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 0cc3dec4..98232389 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -151,6 +151,12 @@ Cmd :: { ## exit_code = Cmd.new("cat").arg("non_existent.txt").exec_exit_code!()? ## ``` exec_exit_code! : Cmd => Try(I32, [FailedToGetExitCode({ command : Str, err : IOErr }), ..]) + exec_exit_code! = |cmd| { + match host_exec_exit_code!(cmd) { + Ok(num) => Ok(num) + Err(io_err) => Err(FailedToGetExitCode({ command : to_str(cmd), err: io_err })) + } + } ## Create a new command with the given program name. Use a function that starts with `exec_` to execute it. ## @@ -232,21 +238,23 @@ Cmd :: { envs: cmd.envs, program: cmd.program, } +} - to_str : Cmd -> Str - to_str = |cmd| { - my_trim = |trimmed_str| {if trimmed_str.is_empty() "" else "envs: ${trimmed_str}"} +host_exec_exit_code! : Cmd => Try(I32, IOErr) - envs_str = - cmd.envs - # TODO once we're using List of tuples: .map(|(key, value)| "${key}=${value}") - .join_with(" ") - .trim()->my_trim() +to_str : Cmd -> Str +to_str = |cmd| { + my_trim = |trimmed_str| {if trimmed_str.is_empty() "" else "envs: ${trimmed_str}"} - clear_envs_str = if cmd.clear_envs ", clear_envs: true" else "" - - \\{ cmd: ${cmd.program}, args: ${Str.join_with(cmd.args, " ")}${envs_str}${clear_envs_str} } - } + envs_str = + cmd.envs + # TODO once we're using List of tuples: .map(|(key, value)| "${key}=${value}") + .join_with(" ") + .trim()->my_trim() + + clear_envs_str = if cmd.clear_envs ", clear_envs: true" else "" + + \\{ cmd: ${cmd.program}, args: ${Str.join_with(cmd.args, " ")}${envs_str}${clear_envs_str} } } # Do not change the order of the fields! It will lead to a segfault. diff --git a/platform/CmdInternal.roc b/platform/CmdInternal.roc deleted file mode 100644 index 0778867a..00000000 --- a/platform/CmdInternal.roc +++ /dev/null @@ -1,12 +0,0 @@ -import IOErr exposing [IOErr] - -CmdInternal :: { - args : List(Str), - clear_envs : Bool, - envs : List(Str), - program : Str, -}.{ - command_exec_exit_code! : CmdInternal => Try(I32, IOErr) - - #command_exec_output! : CmdInternal => Try(OutputFromHostSuccess, (Try(OutputFromHostFailure, IOErr))) -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 0f766605..e68d6464 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,6 @@ use std::io::{self, BufRead, Read, Write}; use std::sync::atomic::{AtomicBool, Ordering}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use roc_command::FailedToGetExitCodeContent; use roc_std_new::{ HostedFn, HostedFunctions, RocAlloc, RocCrashed, RocDbg, RocDealloc, RocExpectFailed, RocList, RocOps, RocRealloc, RocStr, RocTry, @@ -265,7 +264,7 @@ extern "C" fn roc_crashed_fn(roc_crashed: *const RocCrashed, _env: *mut c_void) /// Hosted function: Cmd.exec_exit_code! (index 0) /// Takes Command, returns Try(I32, IOErr) -extern "C" fn hosted_cmd_exec_exit_code( +extern "C" fn hosted_cmd_host_exec_exit_code( ops: *const RocOps, ret_ptr: *mut c_void, args_ptr: *mut c_void, @@ -275,18 +274,11 @@ extern "C" fn hosted_cmd_exec_exit_code( let exec_try = match roc_command::command_exec_exit_code(cmd, roc_ops) { Ok(code) => RocTry::ok(code), - Err(io_err) => { - let cmd_as_str = RocStr::from_str(&cmd.to_string(), roc_ops); - RocTry::err( - RocSingleTagWrapper::new( - roc_command::FailedToGetExitCodeContent{command: cmd_as_str, err: io_err} - ) - ) - }, + Err(io_err) => RocTry::err(io_err), }; unsafe { - std::ptr::write(ret_ptr as *mut RocTry>, exec_try); + std::ptr::write(ret_ptr as *mut RocTry, exec_try); } } @@ -1229,7 +1221,7 @@ extern "C" fn hosted_utc_now(_ops: *const RocOps, ret_ptr: *mut c_void, _args_pt /// Array of hosted function pointers, sorted alphabetically by fully-qualified name. /// IMPORTANT: Order must match the order Roc expects based on alphabetical sorting. static HOSTED_FNS: [HostedFn; 34] = [ - hosted_cmd_exec_exit_code, // 0: Cmd.exec_exit_code! + hosted_cmd_host_exec_exit_code, // 0: Cmd.exec_exit_code! //hosted_cmd_exec_output, // 1: Cmd.exec_output! hosted_dir_create, // 2: Dir.create! hosted_dir_create_all, // 3: Dir.create_all! From ee37aca2dc3060ac4bc0a956c8a7992cb8a7648e Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:44:15 +0100 Subject: [PATCH 05/11] exec, exec_cmd and exec_exit_code works Currently requires Roc branch fix-match-open-union --- examples/command.roc | 32 +++++++++++------------------- platform/Cmd.roc | 46 ++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/examples/command.roc b/examples/command.roc index d673b640..75745ec0 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -7,7 +7,7 @@ import pf.Cmd main! = |_args| { # Simplest way to execute a command (prints to your terminal). - #Cmd.exec!("echo", ["Hello"])? + Cmd.exec!("echo", ["Hello"])? # To execute and capture the output (stdout and stderr) without inheriting your terminal. #cmd_output = @@ -18,31 +18,21 @@ main! = |_args| { #Stdout.line!("${Str.inspect(cmd_output)}")? # To run a command with environment variables. - #Cmd.new("env") - # .clear_envs() # You probably don't need to clear all other environment variables, this is just an example. - # .env("FOO", "BAR") - # .envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs` - # .args(["-v"]) - # .exec_cmd!()? + Cmd.new("env") + .clear_envs() # You probably don't need to clear all other environment variables, this is just an example. + .env("FOO", "BAR") + .envs([("BAZ", "DUCK"), ("XYZ", "ABC")]) # Set multiple environment variables at once with `envs` + .args(["-v"]) + .exec_cmd!()? # To execute and just get the exit code (prints to your terminal). # Prefer using `exec!` or `exec_cmd!`. - output1 = - Cmd.new("cattt") # line! prints do work if this is just `cat` (this is the non-error case) + exit_code = + Cmd.new("cat") .args(["non_existent.txt"]) - .exec_exit_code!() + .exec_exit_code!()? - Stdout.line!("pre Inspect1")? - - Stdout.line!("Inspect1: ${Str.inspect(output1)}")? - - Stdout.line!("post Inspect1")? - - output2 = output1? - - Stdout.line!("Inspect2: ${Str.inspect(output2)}")? - - Stdout.line!("done")? + Stdout.line!("Exit code: ${exit_code.to_str()}")? # TODO add exec_output_bytes diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 98232389..597c0b90 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -15,20 +15,20 @@ Cmd :: { ## ```roc ## Cmd.exec!("echo", ["hello world"])? ## ``` - #exec! : Str, List(Str) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode { command : Str, err : IOErr }, ..]) - #exec! = |program, arguments| { - # exit_code = - # new(program) - # .args(arguments) - # .exec_exit_code!()? + exec! : Str, List(Str) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode({ command : Str, err : IOErr }), ..]) + exec! = |program, arguments| { + exit_code = + new(program) + .args(arguments) + .exec_exit_code!()? - # if exit_code == 0 { - # Ok({}) - # } else { - # command = "${cmd_name} ${arguments.join_with(" ")}" - # Err(ExecFailed({ command, exit_code })) - # } - #} + if exit_code == 0 { + Ok({}) + } else { + command = "${program} ${arguments.join_with(" ")}" + Err(ExecFailed({ command, exit_code })) + } + } ## Execute a Cmd (using the builder pattern). ## Stdin, stdout, and stderr are inherited from the parent process. @@ -42,16 +42,16 @@ Cmd :: { ## .env("RUST_BACKTRACE", "1") ## .exec_cmd!()? ## ``` - #exec_cmd! : Cmd => Try({}, [ExecCmdFailed { command : Str, exit_code : I32 }, FailedToGetExitCode { command : Str, err : IOErr }, ..]) - #exec_cmd! = |cmd| { - # exit_code = exec_exit_code!(cmd)? - # - # if exit_code == 0 { - # Ok({}) - # } else { - # Err(ExecCmdFailed({ command: to_str(cmd), exit_code })) - # } - #} + exec_cmd! : Cmd => Try({}, [ExecCmdFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode({ command : Str, err : IOErr }), ..]) + exec_cmd! = |cmd| { + exit_code = exec_exit_code!(cmd)? + + if exit_code == 0 { + Ok({}) + } else { + Err(ExecCmdFailed({ command: to_str(cmd), exit_code })) + } + } ## Execute command and capture stdout and stderr as UTF-8 strings. ## Invalid UTF-8 sequences are replaced with the Unicode replacement character. From 4039846cbaa3ad70742d152c60268e400f5d24cf Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:28:24 +0100 Subject: [PATCH 06/11] more progress on Cmd refactor --- crates/roc_command/src/lib.rs | 98 ++++++++++++++++++++++------------- examples/command.roc | 10 ++-- platform/Cmd.roc | 44 +++++++++------- src/lib.rs | 34 ++++++------ 4 files changed, 109 insertions(+), 77 deletions(-) diff --git a/crates/roc_command/src/lib.rs b/crates/roc_command/src/lib.rs index a1c5ba99..e052b670 100644 --- a/crates/roc_command/src/lib.rs +++ b/crates/roc_command/src/lib.rs @@ -1,7 +1,7 @@ //! This crate provides common functionality for Roc to interface with `std::process::Command` use roc_io_error::IOErr; -use roc_std_new::{RocList, RocOps, RocRefcounted, RocStr}; +use roc_std_new::{RocList, RocOps, RocRefcounted, RocStr, RocTry}; /// Command struct matching the Roc record memory layout. /// @@ -62,24 +62,50 @@ impl Command { } /// Output when command succeeds (exit code 0) -/// Roc type: { stdout_utf8 : Str, stderr_utf8_lossy : Str } +/// Roc type: {stderr_bytes : List(U8), stdout_bytes : List(U8) } /// Memory layout: Fields sorted by size descending, then alphabetically. -/// Both RocStr are 24 bytes, so alphabetical: stderr_utf8_lossy, stdout_utf8 +/// Both RocList are 24 bytes, so alphabetical: stderr_bytes, stdout_bytes #[derive(Clone, Debug)] #[repr(C)] pub struct CommandOutputSuccess { - pub stderr_utf8_lossy: RocStr, // offset 0 (24 bytes) - pub stdout_utf8: RocStr, // offset 24 (24 bytes) + pub stderr_bytes: RocList, // offset 0 (24 bytes) + pub stdout_bytes: RocList, // offset 24 (24 bytes) } impl RocRefcounted for CommandOutputSuccess { fn inc(&mut self) { - self.stderr_utf8_lossy.inc(); - self.stdout_utf8.inc(); + self.stderr_bytes.inc(); + self.stdout_bytes.inc(); } fn dec(&mut self) { - self.stderr_utf8_lossy.dec(); - self.stdout_utf8.dec(); + self.stderr_bytes.dec(); + self.stdout_bytes.dec(); + } + fn is_refcounted() -> bool { + true + } +} + +/// Output when command fails with non-zero exit code +/// Roc type: {stderr_bytes : List(U8), stdout_bytes : List(U8), exit_code: I32 } +/// Memory layout: Fields sorted by size descending, then alphabetically. +/// RocList (24 bytes) > I32 (4 bytes), so: stderr_bytes (24), stdout_bytes (24), exit_code (4) +#[derive(Clone, Debug)] +#[repr(C)] +pub struct CommandOutputFailure { + pub stderr_bytes: RocList, // offset 0 (24 bytes) + pub stdout_bytes: RocList, // offset 24 (24 bytes) + pub exit_code: i32, // offset 48 (4 bytes + padding) +} + +impl RocRefcounted for CommandOutputFailure { + fn inc(&mut self) { + self.stderr_bytes.inc(); + self.stdout_bytes.inc(); + } + fn dec(&mut self) { + self.stderr_bytes.dec(); + self.stdout_bytes.dec(); } fn is_refcounted() -> bool { true @@ -97,29 +123,31 @@ pub fn command_exec_exit_code(cmd: &Command, roc_ops: &RocOps) -> Result CommandOutputResult { -// match cmd.to_std_command().output() { -// Ok(output) => { -// let stdout_utf8 = bytes_to_roc_str_lossy(&output.stdout, roc_ops); -// let stderr_utf8_lossy = bytes_to_roc_str_lossy(&output.stderr, roc_ops); - -// match output.status.code() { -// Some(0) => CommandOutputResult::Success(CommandOutputSuccess { -// stderr_utf8_lossy, -// stdout_utf8, -// }), -// Some(exit_code) => CommandOutputResult::NonZeroExit(CommandOutputFailure { -// stderr_utf8_lossy, -// stdout_utf8_lossy: stdout_utf8, -// exit_code, -// }), -// None => CommandOutputResult::Error( -// IOErr::new_other("Process was killed by signal", roc_ops) -// ), -// } -// } -// Err(e) => CommandOutputResult::Error(IOErr::from_io_error(&e, roc_ops)), -// } -// } +pub type CommandOutputTry = RocTry>; + +/// Execute command and capture stdout/stderr as UTF-8 strings. +/// Invalid UTF-8 sequences are replaced with the Unicode replacement character. +pub fn command_exec_output(cmd: &Command, roc_ops: &RocOps) -> CommandOutputTry { + match cmd.to_std_command().output() { + Ok(output) => { + let stdout_bytes = RocList::from_slice(&output.stdout, roc_ops); + let stderr_bytes = RocList::from_slice(&output.stderr, roc_ops); + + match output.status.code() { + Some(0) => RocTry::ok(CommandOutputSuccess { + stderr_bytes, + stdout_bytes, + }), + Some(exit_code) => RocTry::err(RocTry::ok(CommandOutputFailure { + stderr_bytes, + stdout_bytes, + exit_code, + })), + None => RocTry::err(RocTry::err( + IOErr::new_other("Process was killed by signal", roc_ops) + )), + } + } + Err(e) => RocTry::err(RocTry::err(IOErr::from_io_error(&e, roc_ops))), + } +} diff --git a/examples/command.roc b/examples/command.roc index 75745ec0..025e0513 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -10,12 +10,12 @@ main! = |_args| { Cmd.exec!("echo", ["Hello"])? # To execute and capture the output (stdout and stderr) without inheriting your terminal. - #cmd_output = - # Cmd.new("echo") - # .args(["Hi"]) - # .exec_output!()? + cmd_output = + Cmd.new("echo") + .args(["Hi"]) + .exec_output!()? - #Stdout.line!("${Str.inspect(cmd_output)}")? + Stdout.line!("${Str.inspect(cmd_output)}")? # To run a command with environment variables. Cmd.new("env") diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 597c0b90..c7f485df 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -76,31 +76,33 @@ Cmd :: { # .. # ] #) - #exec_output! = |cmd| - # exec_try = CmdInternal.command_exec_output!(cmd) + exec_output! = |cmd| { + exec_try = host_exec_output!(cmd) - # match exec_try { - # Ok({ stderr_bytes, stdout_bytes }) => - # stdout_utf8 = - # Str.from_utf8(stdout_bytes) - # .map_err(|err| StdoutContainsInvalidUtf8({ cmd_str: to_str(cmd), err }))? + match exec_try { + Ok({ stderr_bytes, stdout_bytes }) => { + stdout_utf8 = + Str.from_utf8(stdout_bytes) + .map_err(|err| StdoutContainsInvalidUtf8({ cmd_str: to_str(cmd), err }))? - # stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) + stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) - # Ok({ stdout_utf8, stderr_utf8_lossy }) + Ok({ stdout_utf8, stderr_utf8_lossy }) + } + Err(inside_try) => { + match inside_try { + Ok({ exit_code, stderr_bytes, stdout_bytes }) => + stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes) + stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) - # Err(inside_try) => - # match inside_try { - # Ok({ exit_code, stderr_bytes, stdout_bytes }) => - # stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes) - # stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) + Err(NonZeroExitCode({ command: to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy })) - # Err(NonZeroExitCode({ command: to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy })) - - # Err(err) => - # Err(FailedToGetExitCode({ command: to_str(cmd), err: InternalIOErr.handle_err(err) })) - # } - # } + Err(err) => + Err(FailedToGetExitCode({ command: to_str(cmd), err: InternalIOErr.handle_err(err) })) + } + } + } + } ## Execute command and capture stdout and stderr in the original form as bytes. ## @@ -242,6 +244,8 @@ Cmd :: { host_exec_exit_code! : Cmd => Try(I32, IOErr) +host_exec_output! : Cmd => Try(OutputFromHostSuccess, (Try(OutputFromHostFailure, IOErr))) + to_str : Cmd -> Str to_str = |cmd| { my_trim = |trimmed_str| {if trimmed_str.is_empty() "" else "envs: ${trimmed_str}"} diff --git a/src/lib.rs b/src/lib.rs index e68d6464..a346e686 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,20 +284,20 @@ extern "C" fn hosted_cmd_host_exec_exit_code( /// Hosted function: Cmd.exec_output! (index 1) /// Takes Command, returns Try({ stderr_utf8_lossy, stdout_utf8 }, [CmdErr(IOErr), NonZeroExit(...)]) -// extern "C" fn hosted_cmd_exec_output( -// ops: *const RocOps, -// ret_ptr: *mut c_void, -// args_ptr: *mut c_void, -// ) { -// let roc_ops = unsafe { &*ops }; -// let cmd = unsafe { &*(args_ptr as *const roc_command::Command) }; - -// let result = roc_command::command_exec_output(cmd, roc_ops); - -// unsafe { -// std::ptr::write(ret_ptr as *mut TryCmdOutputResult, result); -// } -// } +extern "C" fn hosted_cmd_host_exec_output( + ops: *const RocOps, + ret_ptr: *mut c_void, + args_ptr: *mut c_void, +) { + let roc_ops = unsafe { &*ops }; + let cmd = unsafe { &*(args_ptr as *const roc_command::Command) }; + + let output_try = roc_command::command_exec_output(cmd, roc_ops); + + unsafe { + std::ptr::write(ret_ptr as *mut roc_command::CommandOutputTry, output_try); + } +} /// Hosted function: Dir.create! (index 2) /// Takes Str, returns Try({}, [DirErr(IOErr)]) @@ -1220,9 +1220,9 @@ extern "C" fn hosted_utc_now(_ops: *const RocOps, ret_ptr: *mut c_void, _args_pt /// Array of hosted function pointers, sorted alphabetically by fully-qualified name. /// IMPORTANT: Order must match the order Roc expects based on alphabetical sorting. -static HOSTED_FNS: [HostedFn; 34] = [ - hosted_cmd_host_exec_exit_code, // 0: Cmd.exec_exit_code! - //hosted_cmd_exec_output, // 1: Cmd.exec_output! +static HOSTED_FNS: [HostedFn; 35] = [ + hosted_cmd_host_exec_exit_code, // 0: Cmd.host_exec_exit_code! + hosted_cmd_host_exec_output, // 1: Cmd.host_exec_output! hosted_dir_create, // 2: Dir.create! hosted_dir_create_all, // 3: Dir.create_all! hosted_dir_delete_all, // 4: Dir.delete_all! From ba02bbd683e5e41e5dbe1e9cab448f4c381a5d24 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:50:06 +0100 Subject: [PATCH 07/11] working exec_output --- platform/Cmd.roc | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/platform/Cmd.roc b/platform/Cmd.roc index c7f485df..1b8b0be9 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -91,14 +91,15 @@ Cmd :: { } Err(inside_try) => { match inside_try { - Ok({ exit_code, stderr_bytes, stdout_bytes }) => + Ok({ exit_code, stderr_bytes, stdout_bytes }) => { stdout_utf8_lossy = Str.from_utf8_lossy(stdout_bytes) stderr_utf8_lossy = Str.from_utf8_lossy(stderr_bytes) Err(NonZeroExitCode({ command: to_str(cmd), exit_code, stdout_utf8_lossy, stderr_utf8_lossy })) - - Err(err) => - Err(FailedToGetExitCode({ command: to_str(cmd), err: InternalIOErr.handle_err(err) })) + } + Err(err) => { + Err(FailedToGetExitCode({ command: to_str(cmd), err })) + } } } } @@ -244,7 +245,20 @@ Cmd :: { host_exec_exit_code! : Cmd => Try(I32, IOErr) -host_exec_output! : Cmd => Try(OutputFromHostSuccess, (Try(OutputFromHostFailure, IOErr))) +# Do not change the order of the fields! It will lead to a segfault. +#OutputFromHostSuccess : { +# stderr_bytes : List(U8), +# stdout_bytes : List(U8), +#} + +# Do not change the order of the fields! It will lead to a segfault. +#OutputFromHostFailure : { +# stderr_bytes : List(U8), +# stdout_bytes : List(U8), +# exit_code : I32, +#} + +host_exec_output! : Cmd => Try({stderr_bytes : List(U8), stdout_bytes : List(U8)}, (Try({stderr_bytes : List(U8), stdout_bytes : List(U8), exit_code : I32}, IOErr))) to_str : Cmd -> Str to_str = |cmd| { @@ -260,16 +274,3 @@ to_str = |cmd| { \\{ cmd: ${cmd.program}, args: ${Str.join_with(cmd.args, " ")}${envs_str}${clear_envs_str} } } - -# Do not change the order of the fields! It will lead to a segfault. -OutputFromHostSuccess : { - stderr_bytes : List(U8), - stdout_bytes : List(U8), -} - -# Do not change the order of the fields! It will lead to a segfault. -OutputFromHostFailure : { - stderr_bytes : List(U8), - stdout_bytes : List(U8), - exit_code : I32, -} From a4f6ede0b83e5ad69064ec7270bf0e1c33601d60 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Sat, 28 Feb 2026 19:29:29 +0100 Subject: [PATCH 08/11] all Cmd functions working --- examples/command.roc | 10 ++++---- platform/Cmd.roc | 54 ++++++++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/examples/command.roc b/examples/command.roc index 025e0513..15c62179 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -38,12 +38,12 @@ main! = |_args| { # To execute and capture the output (stdout and stderr) in the original form as bytes without inheriting your terminal. # Prefer using `exec_output!`. - #cmd_output_bytes = - # Cmd.new("echo") - # .args(["Hi"]) - # .exec_output_bytes!()? + cmd_output_bytes = + Cmd.new("echo") + .args(["Hi"]) + .exec_output_bytes!()? - #Stdout.line!("${Str.inspect(cmd_output_bytes)}")? + Stdout.line!("${Str.inspect(cmd_output_bytes)}")? Ok({}) } diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 1b8b0be9..2b1b3e8b 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -67,15 +67,15 @@ Cmd :: { ## ## Stdout.line!("Echo output: ${cmd_output.stdout_utf8}")? ## ``` - #exec_output! : Cmd => Try( - # { stdout_utf8 : Str, stderr_utf8_lossy : Str }, - # [ - # StdoutContainsInvalidUtf8({ cmd_str : Str, err : [BadUtf8 { index : U64, problem : Str.Utf8Problem }] }), - # NonZeroExitCode({ command : Str, exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str }), - # FailedToGetExitCode({ command : Str, err : IOErr }), - # .. - # ] - #) + exec_output! : Cmd => Try( + { stdout_utf8 : Str, stderr_utf8_lossy : Str }, + [ + StdoutContainsInvalidUtf8({ cmd_str : Str, err : [BadUtf8({ problem : _, index : U64 })] }), + NonZeroExitCode({ command : Str, exit_code : I32, stdout_utf8_lossy : Str, stderr_utf8_lossy : Str }), + FailedToGetExitCode({ command : Str, err : IOErr }), + .. + ] + ) exec_output! = |cmd| { exec_try = host_exec_output!(cmd) @@ -118,30 +118,32 @@ Cmd :: { ## Stdout.line!("${Str.inspect(cmd_output_bytes)}")? # {stderr_bytes: [], stdout_bytes: [72, 105, 10]} ## ``` #exec_output_bytes! : Cmd => Try( - # { stderr_bytes : List(U8), stdout_bytes : List(U8) } + # { stderr_bytes : List(U8), stdout_bytes : List(U8) }, # [ # FailedToGetExitCodeB(IOErr), # TODO: perhaps no need for B? # NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) }), # .. # ] #) - #exec_output_bytes! = |cmd| { - # exec_try = CmdInternal.command_exec_output!(cmd) # TODO + exec_output_bytes! = |cmd| { + exec_try = host_exec_output!(cmd) - # match exec_try { - # Ok({ stderr_bytes, stdout_bytes }) => - # Ok({ stdout_bytes, stderr_bytes }) + match exec_try { + Ok({ stderr_bytes, stdout_bytes }) => + Ok({ stdout_bytes, stderr_bytes }) - # Err(inside_try) => - # match inside_try { - # Ok({ exit_code, stderr_bytes, stdout_bytes }) -> - # Err(NonZeroExitCodeB({ exit_code, stdout_bytes, stderr_bytes })) + Err(inside_try) => + match inside_try { + Ok({ exit_code, stderr_bytes, stdout_bytes }) => { + Err(NonZeroExitCodeB({ exit_code, stdout_bytes, stderr_bytes })) + } - # Err(err) -> - # Err(FailedToGetExitCodeB(InternalIOErr.handle_err(err))) - # } - # } - #} + Err(err) => { + Err(FailedToGetExitCodeB(err)) + } + } + } + } ## Execute a command and return its exit code. ## Stdin, stdout, and stderr are inherited from the parent process. @@ -245,19 +247,23 @@ Cmd :: { host_exec_exit_code! : Cmd => Try(I32, IOErr) + # Do not change the order of the fields! It will lead to a segfault. +# TODO uncomment once #9216 is fixed #OutputFromHostSuccess : { # stderr_bytes : List(U8), # stdout_bytes : List(U8), #} # Do not change the order of the fields! It will lead to a segfault. +# TODO uncomment once #9216 is fixed #OutputFromHostFailure : { # stderr_bytes : List(U8), # stdout_bytes : List(U8), # exit_code : I32, #} +# TODO use OutputFromHostSuccess and OutputFromHostFailure once #9216 is fixed host_exec_output! : Cmd => Try({stderr_bytes : List(U8), stdout_bytes : List(U8)}, (Try({stderr_bytes : List(U8), stdout_bytes : List(U8), exit_code : I32}, IOErr))) to_str : Cmd -> Str From 3c5aa030bc01b94760e9162536e18da651e2fcad Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:16:34 +0100 Subject: [PATCH 09/11] finish Cmd refactor, ressurect tests --- examples/command.roc | 2 - platform/Cmd.roc | 16 +++--- src/lib.rs | 66 ------------------------ tests/cmd-test.roc | 117 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 76 deletions(-) create mode 100644 tests/cmd-test.roc diff --git a/examples/command.roc b/examples/command.roc index 15c62179..d405b991 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -34,8 +34,6 @@ main! = |_args| { Stdout.line!("Exit code: ${exit_code.to_str()}")? - # TODO add exec_output_bytes - # To execute and capture the output (stdout and stderr) in the original form as bytes without inheriting your terminal. # Prefer using `exec_output!`. cmd_output_bytes = diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 2b1b3e8b..4c911fe2 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -117,14 +117,14 @@ Cmd :: { ## ## Stdout.line!("${Str.inspect(cmd_output_bytes)}")? # {stderr_bytes: [], stdout_bytes: [72, 105, 10]} ## ``` - #exec_output_bytes! : Cmd => Try( - # { stderr_bytes : List(U8), stdout_bytes : List(U8) }, - # [ - # FailedToGetExitCodeB(IOErr), # TODO: perhaps no need for B? - # NonZeroExitCode({ exit_code : I32, stderr_bytes : List(U8), stdout_bytes : List(U8) }), - # .. - # ] - #) + exec_output_bytes! : Cmd => Try( + { stderr_bytes : List(U8), stdout_bytes : List(U8) }, + [ + NonZeroExitCodeB({ exit_code : I32, stdout_bytes : List(U8), stderr_bytes : List(U8) }), + FailedToGetExitCodeB(IOErr), + .. + ] + ) exec_output_bytes! = |cmd| { exec_try = host_exec_output!(cmd) diff --git a/src/lib.rs b/src/lib.rs index a346e686..a7e5bcb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -192,72 +192,6 @@ extern "C" fn roc_crashed_fn(roc_crashed: *const RocCrashed, _env: *mut c_void) } } -// ============================================================================ -// Cmd Module Types and Functions -// ============================================================================ - -/// Output record: { stderr_utf8_lossy : Str, stdout_utf8 : Str } -/// Memory layout: Both RocLists are 24 bytes, alphabetical: stderr_utf8_lossy, stdout_utf8 -// #[repr(C)] -// pub struct OutputFromHostSuccess { -// pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) -// pub stdout_utf8: RocList, // offset 24 (24 bytes) -// } - -// /// Output record: { exit_code : I32, stderr_utf8_lossy : List(U8), stdout_utf8_lossy : List(U8) } -// /// Memory layout: RocList (24 bytes) > I32 (4 bytes), so: stderr_utf8_lossy, stdout_utf8_lossy, exit_code -// #[repr(C)] -// pub struct OutputFromHostFailure { -// pub stderr_utf8_lossy: RocList, // offset 0 (24 bytes) -// pub stdout_utf8_lossy: RocList, // offset 24 (24 bytes) -// pub exit_code: i32, // offset 48 (4 bytes + padding) -// } - -// /// Error type for command_exec_output!: [CmdErr(IOErr), NonZeroExit({ exit_code, stderr, stdout })] -// /// Alphabetically: CmdErr=0, NonZeroExit=1 -// #[repr(C)] -// pub union CmdOutputErrPayload { -// cmd_err: core::mem::ManuallyDrop, -// non_zero_exit: core::mem::ManuallyDrop, -// } - -// #[repr(C)] -// pub struct CmdOutputErr { -// payload: CmdOutputErrPayload, -// discriminant: u8, // CmdErr=0, NonZeroExit=1 -// } - -// impl CmdOutputErr { -// pub fn cmd_err(io_err: roc_io_error::IOErr) -> Self { -// Self { -// payload: CmdOutputErrPayload { -// cmd_err: core::mem::ManuallyDrop::new(io_err), -// }, -// discriminant: 0, -// } -// } - -// pub fn non_zero_exit( -// stderr_utf8_lossy: RocStr, -// stdout_utf8_lossy: RocStr, -// exit_code: i32, -// ) -> Self { -// Self { -// payload: CmdOutputErrPayload { -// non_zero_exit: core::mem::ManuallyDrop::new(NonZeroExitPayload { -// stderr_utf8_lossy, -// stdout_utf8_lossy, -// exit_code, -// }), -// }, -// discriminant: 1, -// } -// } -// } - -/// Type alias for Try({ stderr, stdout }, [CmdErr(IOErr), NonZeroExit(...)]) - using official RocTry -//type TryCmdOutputResult = RocTry; - // ============================================================================ // Hosted Functions (sorted alphabetically by fully-qualified name) // ============================================================================ diff --git a/tests/cmd-test.roc b/tests/cmd-test.roc new file mode 100644 index 00000000..387bc5e6 --- /dev/null +++ b/tests/cmd-test.roc @@ -0,0 +1,117 @@ +app [main!] { pf: platform "../platform/main.roc" } + +import pf.Stdout +import pf.Cmd + +# Tests all error cases in Cmd functions. + +main! = |_args| { + + # exec! + expect_err( + Cmd.exec!("blablaXYZ", []), + "Try.Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" + )? + + # expect_err( + # Cmd.exec!("cat", ["non_existent.txt"]), + # "(Err (ExecFailed {command: \"cat non_existent.txt\", exit_code: 1}))" + # )? + + # # exec_cmd! + # expect_err( + # Cmd.new("blablaXYZ") + # |> Cmd.exec_cmd!, + # "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" + # )? + + # expect_err( + # Cmd.new("cat") + # |> Cmd.arg("non_existent.txt") + # |> Cmd.exec_cmd!, + # "(Err (ExecCmdFailed {command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1}))" + # )? + + # # exec_output! + # expect_err( + # Cmd.new("blablaXYZ") + # |> Cmd.exec_output!, + # "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" + # )? + + # expect_err( + # Cmd.new("cat") + # |> Cmd.arg("non_existent.txt") + # |> Cmd.exec_output!, + # "(Err (NonZeroExitCode {command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1, stderr_utf8_lossy: \"cat: non_existent.txt: No such file or directory\n\", stdout_utf8_lossy: \"\"}))" + # )? + + # # Test StdoutContainsInvalidUtf8 - using printf to output invalid UTF-8 bytes + # expect_err( + # Cmd.new("printf") + # |> Cmd.args(["\\377\\376"]) # Invalid UTF-8 sequence + # |> Cmd.exec_output!, + # "(Err (StdoutContainsInvalidUtf8 {cmd_str: \"{ cmd: printf, args: \\377\\376 }\", err: (BadUtf8 {index: 0, problem: InvalidStartByte})}))" + # )? + + # # exec_output_bytes! + # expect_err( + # Cmd.new("blablaXYZ") + # |> Cmd.exec_output_bytes!, + # "(Err (FailedToGetExitCodeB NotFound))" + # )? + + # expect_err( + # Cmd.new("cat") + # |> Cmd.arg("non_existent.txt") + # |> Cmd.exec_output_bytes!, + # "(Err (NonZeroExitCodeB {exit_code: 1, stderr_bytes: [99, 97, 116, 58, 32, 110, 111, 110, 95, 101, 120, 105, 115, 116, 101, 110, 116, 46, 116, 120, 116, 58, 32, 78, 111, 32, 115, 117, 99, 104, 32, 102, 105, 108, 101, 32, 111, 114, 32, 100, 105, 114, 101, 99, 116, 111, 114, 121, 10], stdout_bytes: []}))" + # )? + + # # exec_exit_code! + # expect_err( + # Cmd.new("blablaXYZ") + # |> Cmd.exec_exit_code!, + # "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" + # )? + + # # exec_exit_code! with non-zero exit code is not an error - it returns the exit code + # exit_code = + # Cmd.new("cat") + # |> Cmd.arg("non_existent.txt") + # |> Cmd.exec_exit_code!()? + # + # if exit_code == 1 { + # Ok({})? + # } else { + # Err(FailedExpectation( + # """ + # + # - Expected: + # 1 + # + # - Got: + # ${Inspect.to_str(exit_code)} + # + # """ + # ))? + # } + + Stdout.line!("All tests passed.")? + + Ok({}) +} + +expect_err = |err, expected_str| { + if Str.inspect(err) == expected_str { + Ok({}) + } else { + Err(FailedExpectation( + \\- Expected: + \\${expected_str} + + \\- Got: + \\${Str.inspect(err)} + )) + } +} \ No newline at end of file From acdd26f4fa665512dc1b47bea89fea84a77f78c0 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:32:51 +0100 Subject: [PATCH 10/11] enable+convert more cmd-test.roc --- Cargo.lock | 2 +- Cargo.toml | 4 +- tests/cmd-test.roc | 128 ++++++++++++++++++++------------------------- 3 files changed, 59 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6f576f9..88141885 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "roc_std_new" version = "0.0.1" -source = "git+https://github.com/roc-lang/roc?rev=c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f#c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f" +source = "git+https://github.com/roc-lang/roc?rev=3fa8df31997e3e233d5bda63eec8737ccd3ce941#3fa8df31997e3e233d5bda63eec8737ccd3ce941" dependencies = [ "arrayvec", "static_assertions", diff --git a/Cargo.toml b/Cargo.toml index fb321be1..7a81dab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,8 +39,8 @@ repository = "https://github.com/roc-lang/basic-cli" [workspace.dependencies] # Core Roc types -# roc-nightly: 2026-02-20 -roc_std_new = { git = "https://github.com/roc-lang/roc", rev = "c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f" } +# roc-nightly: 2026-03-03 +roc_std_new = { git = "https://github.com/roc-lang/roc", rev = "3fa8df31997e3e233d5bda63eec8737ccd3ce941" } # Internal crates roc_io_error = { path = "crates/roc_io_error" } diff --git a/tests/cmd-test.roc b/tests/cmd-test.roc index 387bc5e6..085890f3 100644 --- a/tests/cmd-test.roc +++ b/tests/cmd-test.roc @@ -13,89 +13,73 @@ main! = |_args| { "Try.Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" )? - # expect_err( - # Cmd.exec!("cat", ["non_existent.txt"]), - # "(Err (ExecFailed {command: \"cat non_existent.txt\", exit_code: 1}))" - # )? + expect_err( + Cmd.exec!("cat", ["non_existent.txt"]), + "Try.Err(ExecFailed({ command: \"cat non_existent.txt\", exit_code: 1 }))" + )? - # # exec_cmd! - # expect_err( - # Cmd.new("blablaXYZ") - # |> Cmd.exec_cmd!, - # "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" - # )? + # exec_cmd! + expect_err( + Cmd.new("blablaXYZ").exec_cmd!(), + "Try.Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" + )? - # expect_err( - # Cmd.new("cat") - # |> Cmd.arg("non_existent.txt") - # |> Cmd.exec_cmd!, - # "(Err (ExecCmdFailed {command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1}))" - # )? + expect_err( + Cmd.new("cat").arg("non_existent.txt").exec_cmd!(), + "Try.Err(ExecCmdFailed({ command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1 }))" + )? - # # exec_output! - # expect_err( - # Cmd.new("blablaXYZ") - # |> Cmd.exec_output!, - # "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" - # )? + # exec_output! + expect_err( + Cmd.new("blablaXYZ").exec_output!(), + "Try.Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" + )? - # expect_err( - # Cmd.new("cat") - # |> Cmd.arg("non_existent.txt") - # |> Cmd.exec_output!, - # "(Err (NonZeroExitCode {command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1, stderr_utf8_lossy: \"cat: non_existent.txt: No such file or directory\n\", stdout_utf8_lossy: \"\"}))" - # )? + expect_err( + Cmd.new("cat").arg("non_existent.txt").exec_output!(), + "Try.Err(NonZeroExitCode({ command: \"{ cmd: cat, args: non_existent.txt }\", exit_code: 1, stderr_utf8_lossy: \"cat: non_existent.txt: No such file or directory\n\", stdout_utf8_lossy: \"\" }))" + )? - # # Test StdoutContainsInvalidUtf8 - using printf to output invalid UTF-8 bytes + # Test StdoutContainsInvalidUtf8 - blocked by compiler bug # expect_err( - # Cmd.new("printf") - # |> Cmd.args(["\\377\\376"]) # Invalid UTF-8 sequence - # |> Cmd.exec_output!, - # "(Err (StdoutContainsInvalidUtf8 {cmd_str: \"{ cmd: printf, args: \\377\\376 }\", err: (BadUtf8 {index: 0, problem: InvalidStartByte})}))" + # Cmd.new("printf").args(["\\377\\376"]).exec_output!(), + # "Try.Err(StdoutContainsInvalidUtf8({ cmd_str: \"{ cmd: printf, args: \\377\\376 }\", err: BadUtf8({ index: 0, problem: InvalidStartByte }) }))" # )? - # # exec_output_bytes! - # expect_err( - # Cmd.new("blablaXYZ") - # |> Cmd.exec_output_bytes!, - # "(Err (FailedToGetExitCodeB NotFound))" - # )? + # exec_output_bytes! + expect_err( + Cmd.new("blablaXYZ").exec_output_bytes!(), + "Try.Err(FailedToGetExitCodeB(NotFound))" + )? - # expect_err( - # Cmd.new("cat") - # |> Cmd.arg("non_existent.txt") - # |> Cmd.exec_output_bytes!, - # "(Err (NonZeroExitCodeB {exit_code: 1, stderr_bytes: [99, 97, 116, 58, 32, 110, 111, 110, 95, 101, 120, 105, 115, 116, 101, 110, 116, 46, 116, 120, 116, 58, 32, 78, 111, 32, 115, 117, 99, 104, 32, 102, 105, 108, 101, 32, 111, 114, 32, 100, 105, 114, 101, 99, 116, 111, 114, 121, 10], stdout_bytes: []}))" - # )? + expect_err( + Cmd.new("cat").arg("non_existent.txt").exec_output_bytes!(), + "Try.Err(NonZeroExitCodeB({ exit_code: 1, stderr_bytes: [99, 97, 116, 58, 32, 110, 111, 110, 95, 101, 120, 105, 115, 116, 101, 110, 116, 46, 116, 120, 116, 58, 32, 78, 111, 32, 115, 117, 99, 104, 32, 102, 105, 108, 101, 32, 111, 114, 32, 100, 105, 114, 101, 99, 116, 111, 114, 121, 10], stdout_bytes: [] }))" + )? - # # exec_exit_code! - # expect_err( - # Cmd.new("blablaXYZ") - # |> Cmd.exec_exit_code!, - # "(Err (FailedToGetExitCode {command: \"{ cmd: blablaXYZ, args: }\", err: NotFound}))" - # )? + # exec_exit_code! + expect_err( + Cmd.new("blablaXYZ").exec_exit_code!(), + "Try.Err(FailedToGetExitCode({ command: \"{ cmd: blablaXYZ, args: }\", err: NotFound }))" + )? + + # exec_exit_code! with non-zero exit code is not an error - it returns the exit code + exit_code = + Cmd.new("cat") + .arg("non_existent.txt") + .exec_exit_code!()? - # # exec_exit_code! with non-zero exit code is not an error - it returns the exit code - # exit_code = - # Cmd.new("cat") - # |> Cmd.arg("non_existent.txt") - # |> Cmd.exec_exit_code!()? - # - # if exit_code == 1 { - # Ok({})? - # } else { - # Err(FailedExpectation( - # """ - # - # - Expected: - # 1 - # - # - Got: - # ${Inspect.to_str(exit_code)} - # - # """ - # ))? - # } + if exit_code == 1 { + Ok({})? + } else { + Err(FailedExpectation( + \\- Expected: + \\1 + \\ + \\- Got: + \\${Str.inspect(exit_code)} + ))? + } Stdout.line!("All tests passed.")? From 4637d6391428c5863117828c36a23d406ad6c3e4 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:57:25 +0100 Subject: [PATCH 11/11] fix command.exp --- ci/expect_scripts/command.exp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/ci/expect_scripts/command.exp b/ci/expect_scripts/command.exp index 81a4616c..abeafdd3 100644 --- a/ci/expect_scripts/command.exp +++ b/ci/expect_scripts/command.exp @@ -9,18 +9,23 @@ source ./ci/expect_scripts/shared-code.exp spawn $env(EXAMPLES_DIR)command -expect "Hello" -expect "{stderr_utf8_lossy: \"\", stdout_utf8: \"Hi" -expect "\"}" -expect "BAZ=DUCK" -expect "FOO=BAR" -expect "XYZ=ABC" -expect "cat: non_existent.txt: No such file or directory" -expect "Exit code: 1" +set expected_output [normalize_output { +Hello +{ stderr_utf8_lossy: "", stdout_utf8: "Hi +" } +BAZ=DUCK +FOO=BAR +XYZ=ABC +cat: non_existent.txt: No such file or directory +Exit code: 1 +{ stderr_bytes: [], stdout_bytes: [72, 105, 10] } +}] -expect eof { - check_exit_and_segfault +expect -exact $expected_output { + expect eof { + check_exit_and_segfault + } } puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." -exit 1 \ No newline at end of file +exit 1