diff --git a/Cargo.lock b/Cargo.lock index 32fd07dd..b6f576f9 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=c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f#c5d87ef595fdc1c8b28dfdb6ebbe5b44ddea966f" 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..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-01-12 -roc_std_new = { git = "https://github.com/roc-lang/roc", rev = "36e9ff29fed0ebe6e9d47e30c14bc2d0a2545a5f" } +# 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/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/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..fd726ef7 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..0cc3dec4 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -1,12 +1,158 @@ +import IOErr exposing [IOErr] + Cmd :: { args : List(Str), clear_envs : Bool, - envs : List(Str), + envs : List(Str), # TODO change this to List((Str, 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 }), ..]) + + ## 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 +166,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 +208,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| { @@ -88,62 +233,31 @@ Cmd :: { 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)]) + to_str : Cmd -> Str + to_str = |cmd| { + my_trim = |trimmed_str| {if trimmed_str.is_empty() "" else "envs: ${trimmed_str}"} - ## 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 })] - ) + envs_str = + cmd.envs + # TODO once we're using List of tuples: .map(|(key, value)| "${key}=${value}") + .join_with(" ") + .trim()->my_trim() - ## 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)), - } + 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} } } +} - ## 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)), - } - } +# 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 new file mode 100644 index 00000000..0778867a --- /dev/null +++ b/platform/CmdInternal.roc @@ -0,0 +1,12 @@ +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/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..0f766605 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,10 +2,11 @@ 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}; +use roc_command::FailedToGetExitCodeContent; use roc_std_new::{ HostedFn, HostedFunctions, RocAlloc, RocCrashed, RocDbg, RocDealloc, RocExpectFailed, RocList, RocOps, RocRealloc, RocStr, RocTry, @@ -196,80 +197,74 @@ 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 -#[repr(C)] -pub struct CmdOutputSuccess { - pub stderr_utf8_lossy: RocStr, // offset 0 (24 bytes) - pub stdout_utf8: RocStr, // 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 -#[repr(C)] -pub struct NonZeroExitPayload { - 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) -} - -/// Error type for 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, - } - } -} +/// 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; +//type TryCmdOutputResult = RocTry; // ============================================================================ // Hosted Functions (sorted alphabetically by fully-qualified name) // ============================================================================ /// 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, @@ -278,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: TryI32CmdErr = match result { - Ok(exit_code) => RocTry::ok(exit_code), - Err(io_err) => RocTry::err(RocSingleTagWrapper::new(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 TryI32CmdErr, 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)]) @@ -448,48 +432,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 +741,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 +890,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: 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); } } -/// Hosted function: Stdin.line! (index 18) -/// Takes {}, returns Str +/// 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)) + } + }; - // Create RocStr - ownership transfers to Roc + 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>; + +/// 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 { - *(ret_ptr as *mut RocStr) = roc_str; - // DO NOT call decref - ownership transferred to Roc + std::ptr::write(ret_ptr as *mut TryBytesStdinLineErr, try_result); } } -/// Hosted function: Stdout.line! (index 19) -/// Takes Str, returns {} +/// 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 { + std::ptr::write(ret_ptr as *mut TryBytesStdinErr, try_result); + } +} + +/// 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 +1228,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; 34] = [ + 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.