diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f8dafb2..6c4c07a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Install Rust toolchain run: | rustup toolchain install "${{ needs.rust-version.outputs.version }}" --profile minimal --target x86_64-unknown-linux-musl --component llvm-tools-preview @@ -63,7 +63,7 @@ jobs: - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Install Rust toolchain shell: bash run: | @@ -108,7 +108,7 @@ jobs: - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Install Rust toolchain shell: bash run: | diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 57a7b99f..019d3114 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -40,7 +40,7 @@ jobs: uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Restore released docs env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83e9b8a8..cb7cfeb4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,7 +114,7 @@ jobs: uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Install Zig uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f @@ -215,7 +215,7 @@ jobs: uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Install Rust shell: bash @@ -260,7 +260,7 @@ jobs: uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Create docs staging tree run: mkdir -p docs-site @@ -408,7 +408,7 @@ jobs: uses: roc-lang/setup-roc@bd311e2fb815a3d2255f7ee14a922f0b736e020b with: version: nightly-new-compiler - nightly-tag: nightly-2026-09-04-c125b82 + nightly-tag: nightly-2026-09-22-e494788 - name: Download release metadata uses: actions/download-artifact@v8 diff --git a/examples/file-size.roc b/examples/file-size.roc index c03e0adf..0f29fd59 100644 --- a/examples/file-size.roc +++ b/examples/file-size.roc @@ -19,7 +19,7 @@ main! = |args| { Ok({}) } -path_argument : List(OsStr) -> Try(Path, [MissingPathArgument, ..]) +path_argument : List(OsStr) -> Try(Path, [MissingPathArgument]) path_argument = |args| match args { [first, ..] => Ok(Path.from_os_str(first)) diff --git a/examples/http-client.roc b/examples/http-client.roc index df53c0fa..946af5df 100644 --- a/examples/http-client.roc +++ b/examples/http-client.roc @@ -87,4 +87,8 @@ reject_invalid_utf8! = || { } write_line! : Str => Try({}, _) -write_line! = |message| Stdout.line!(message) +write_line! = |message| + match Stdout.line!(message) { + Ok(value) => Ok(value) + Err(StdoutErr(err)) => Err(StdoutErr(err)) + } diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 47b9bbb7..90bfae19 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -45,7 +45,7 @@ Cmd :: { ## ```roc ## Cmd.exec!("echo", ["hello world"])? ## ``` - exec! : OsStr, List(OsStr) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode({ command : Str, err : IOErr }), ..]) + exec! : OsStr, List(OsStr) => Try({}, [ExecFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode({ command : Str, err : IOErr })]) exec! = |program, arguments| { command = "${OsStr.display(program)} ${Str.join_with(arguments.map(OsStr.display), " ")}" @@ -72,7 +72,7 @@ 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 => Try({}, [ExecCmdFailed({ command : Str, exit_code : I32 }), FailedToGetExitCode({ command : Str, err : IOErr })]) exec_cmd! = |cmd| { command = to_str(cmd) exit_code = exec_exit_code!(cmd)? @@ -98,7 +98,7 @@ 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({ 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 => 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| { cmd_str = to_str(cmd) exec_try = Host.cmd_exec_output!(to_host_cmd(cmd)) @@ -136,7 +136,7 @@ Cmd :: { ## ## Stdout.line!("${Str.inspect(cmd_output)}")? # {stderr_bytes: [], stdout_bytes: [72, 105, 10]} ## ``` - 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 => 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.cmd_exec_output!(to_host_cmd(cmd)) @@ -164,7 +164,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 => Try(I32, [FailedToGetExitCode({ command : Str, err : IOErr })]) exec_exit_code! = |cmd| { command = to_str(cmd) diff --git a/platform/Env.roc b/platform/Env.roc index 33efca6a..4c18076b 100644 --- a/platform/Env.roc +++ b/platform/Env.roc @@ -33,7 +33,7 @@ Env :: [].{ ## Reads the given environment variable. ## ## Returns `Err(VarNotFound(name))` if the variable is not set. - var! : OsStr => Try(OsStr, [VarNotFound(OsStr), EnvErr(IOErr), ..]) + var! : OsStr => Try(OsStr, [VarNotFound(OsStr), EnvErr(IOErr)]) var! = |name| match Host.env_var!(OsStr.to_raw(name)) { Ok(raw) => Ok(OsStr.from_raw(raw)) @@ -42,7 +42,7 @@ Env :: [].{ } ## Reads the given environment variable as a string if its native value is valid text. - var_str! : OsStr => Try(Str, [VarNotFound(OsStr), EnvErr(IOErr), InvalidStr(U64), ..]) + var_str! : OsStr => Try(Str, [VarNotFound(OsStr), EnvErr(IOErr), InvalidStr(U64)]) var_str! = |name| match var!(name) { Ok(value) => @@ -58,7 +58,7 @@ Env :: [].{ ## from the environment. ## ## Returns `Err(CwdUnavailable)` if the cwd cannot be determined. - cwd! : () => Try(Path.Path, [CwdUnavailable, ..]) + cwd! : () => Try(Path.Path, [CwdUnavailable]) cwd! = || match Host.env_cwd!() { Ok(raw) => Ok(Path.from_raw(raw)) @@ -69,7 +69,7 @@ Env :: [].{ ## ## Returns `Err(InvalidCwd(err))` when the path cannot be used as a working ## directory. The process-wide change remains in effect until changed again. - set_cwd! : Path.Path => Try({}, [InvalidCwd(IOErr), ..]) + set_cwd! : Path.Path => Try({}, [InvalidCwd(IOErr)]) set_cwd! = |path| Host.env_set_cwd!(Path.to_raw(path)) .map_err(|err| InvalidCwd(err)) @@ -77,7 +77,7 @@ Env :: [].{ ## Gets the path to the currently-running executable. ## ## Returns `Err(ExePathUnavailable)` if the path cannot be determined. - exe_path! : () => Try(Path.Path, [ExePathUnavailable, ..]) + exe_path! : () => Try(Path.Path, [ExePathUnavailable]) exe_path! = || match Host.env_exe_path!() { Ok(raw) => Ok(Path.from_raw(raw)) @@ -90,7 +90,7 @@ Env :: [].{ ## another value. Unlike [`exe_path!`](#exe_path!), it is returned as an ## [`OsStr`](OsStr), preserving the value exactly without treating it as a path. ## Returns `Err(ProgramNameUnavailable)` if the launcher supplied no first argument. - program_name! : () => Try(OsStr, [ProgramNameUnavailable, ..]) + program_name! : () => Try(OsStr, [ProgramNameUnavailable]) program_name! = || match Host.env_program_name!() { Ok(raw) => Ok(OsStr.from_raw(raw)) @@ -99,16 +99,16 @@ Env :: [].{ ## Atomically create a private directory in the system temporary directory. ## The caller owns cleanup. Unix directories have mode 0700. - create_temp_dir! : () => Try(Path.Path, [TempDirErr(IOErr), ..]) + create_temp_dir! : () => Try(Path.Path, [TempDirErr(IOErr)]) create_temp_dir! = || create_temp_dir_with_prefix!("roc-") ## Create a private temporary directory with a filename prefix. - create_temp_dir_with_prefix! : Str => Try(Path.Path, [TempDirErr(IOErr), ..]) + create_temp_dir_with_prefix! : Str => Try(Path.Path, [TempDirErr(IOErr)]) create_temp_dir_with_prefix! = |prefix| create_temp_dir_in!(temp_dir!(), prefix) ## Create a private temporary directory under an existing parent directory. ## Prefixes cannot contain separators, colons, NUL, or be `.` or `..`. - create_temp_dir_in! : Path.Path, Str => Try(Path.Path, [TempDirErr(IOErr), ..]) + create_temp_dir_in! : Path.Path, Str => Try(Path.Path, [TempDirErr(IOErr)]) create_temp_dir_in! = |parent, prefix| Host.env_create_temp_dir!(Path.to_raw(parent), prefix).map_ok(Path.from_raw).map_err(|err| TempDirErr(err)) @@ -122,7 +122,7 @@ Env :: [].{ ## Path.read_utf8!(file) ## })? ## ``` - with_temp_dir! : (Path.Path => Try(a, err)) => Try(a, [TempDirErr(IOErr), CallbackErr(err), CleanupErr(IOErr, Path.Path), CallbackAndCleanupErr(err, IOErr, Path.Path), ..]) + with_temp_dir! : (Path.Path => Try(a, err)) => Try(a, [TempDirErr(IOErr), CallbackErr(err), CleanupErr(IOErr, Path.Path), CallbackAndCleanupErr(err, IOErr, Path.Path)]) with_temp_dir! = |callback!| { path = create_temp_dir!()? result = callback!(path) diff --git a/platform/Http.roc b/platform/Http.roc index bbec0fd3..6fb3c6a6 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -27,7 +27,7 @@ Http :: [].{ ## request = Request.from_method(GET).with_uri("https://www.roc-lang.org") ## response = Http.send!(request)? ## ``` - send! : Request => Try(Response, [InvalidUrl(Url.ParseErr), HttpErr(TransportErr), ..]) + send! : Request => Try(Response, [InvalidUrl(Url.ParseErr), HttpErr(TransportErr)]) send! = |request| { url = Url.parse(Request.uri(request)) ? InvalidUrl canonical_url = Url.without_fragment(url) @@ -41,7 +41,7 @@ Http :: [].{ ## ## This uses Roc's builtin JSON encoder, so the value's type determines the ## encoder through static dispatch. - with_json_body : Request, _ => Try(Request, [JsonErr(_), ..]) + with_json_body : Request, _ => Try(Request, [JsonErr(_)]) with_json_body = |request, value| { body = Json.to_str_try(value) ? JsonErr @@ -53,7 +53,7 @@ Http :: [].{ } ## Encode a value as JSON, attach it to the request body, and send it. - send_json! : Request, _ => Try(Response, [JsonErr(_), InvalidUrl(Url.ParseErr), HttpErr(TransportErr), ..]) + send_json! : Request, _ => Try(Response, [JsonErr(_), InvalidUrl(Url.ParseErr), HttpErr(TransportErr)]) send_json! = |request, value| { json_request = with_json_body(request, value)? @@ -68,7 +68,7 @@ Http :: [].{ ## ```roc ## hello_str = Http.get_utf8!("http://localhost:8000")? ## ``` - get_utf8! : Url.Url => Try(Str, [BadBody(Str), InvalidUrl(Url.ParseErr), HttpErr(TransportErr), ..]) + get_utf8! : Url.Url => Try(Str, [BadBody(Str), InvalidUrl(Url.ParseErr), HttpErr(TransportErr)]) get_utf8! = |url| { response = send!(Request.from_method(GET).with_uri(Url.to_str(url)))? body = Str.from_utf8(Response.body(response)) ? |_| BadBody("get_utf8!: response body was not valid UTF-8") @@ -80,7 +80,7 @@ Http :: [].{ ## ## This uses Roc's builtin JSON parser, so the expected result type ## determines the parser through static dispatch. - decode_json_response : Response => Try(_, [BadBody(Str), JsonErr(_), ..]) + decode_json_response : Response => Try(_, [BadBody(Str), JsonErr(_)]) decode_json_response = |response| { body = Str.from_utf8(Response.body(response)) ? |_| BadBody("decode_json_response: response body was not valid UTF-8") decoded = Json.parse(body) ? JsonErr @@ -97,7 +97,7 @@ Http :: [].{ ## payload : Try({ foo : Str }, _) ## payload = Http.get!("http://localhost:8000") ## ``` - get! : Url.Url => Try(_, [BadBody(Str), InvalidUrl(Url.ParseErr), HttpErr(TransportErr), JsonErr(_), ..]) + get! : Url.Url => Try(_, [BadBody(Str), InvalidUrl(Url.ParseErr), HttpErr(TransportErr), JsonErr(_)]) get! = |url| { response = send!(Request.from_method(GET).with_uri(Url.to_str(url)))? diff --git a/platform/Locale.roc b/platform/Locale.roc index 171ed556..be8affba 100644 --- a/platform/Locale.roc +++ b/platform/Locale.roc @@ -91,9 +91,9 @@ Locale :: { raw : Str }.{ ## `POSIX` locales are not language tags and are ignored. ## ## Returns `Err(NotAvailable)` if the locale cannot be determined. - get! : () => Try(Locale, [NotAvailable, ..]) + get! : () => Try(Locale, [NotAvailable]) get! = || { - raw = widen_locale_err(Host.locale_get!())? + raw = Host.locale_get!()? Ok(Locale.{ raw }) } @@ -213,13 +213,6 @@ parse_err_to_str = |err| SubtagTooLong => "Locale subtags must be at most 8 characters" } -widen_locale_err : Try(a, [NotAvailable]) -> Try(a, [NotAvailable, ..]) -widen_locale_err = |result| - match result { - Ok(value) => Ok(value) - Err(NotAvailable) => Err(NotAvailable) - } - expect match Locale.parse("en-US") { Ok(locale) => Locale.to_str(locale) == "en-US" diff --git a/platform/Path.roc b/platform/Path.roc index 93b96847..d248368d 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -41,7 +41,7 @@ Path := [ ## This function does not traverse symbolic links; symbolic links (including ## broken ones) return `Bool.False`. ## Sockets, FIFOs, devices, and other special filesystem objects also return `Bool.False`. - is_file! : Path => Try(Bool, [PathErr(IOErr, Path), ..]) + is_file! : Path => Try(Bool, [PathErr(IOErr, Path)]) is_file! = |path| match type!(path) { Ok(IsFile) => Ok(Bool.True) @@ -55,7 +55,7 @@ Path := [ ## This function does not traverse symbolic links; symbolic links (including ## broken ones) return `Bool.False`. ## Sockets, FIFOs, devices, and other special filesystem objects also return `Bool.False`. - is_dir! : Path => Try(Bool, [PathErr(IOErr, Path), ..]) + is_dir! : Path => Try(Bool, [PathErr(IOErr, Path)]) is_dir! = |path| match type!(path) { Ok(IsDir) => Ok(Bool.True) @@ -69,7 +69,7 @@ Path := [ ## This function will not traverse symbolic links - it checks whether the path ## itself is a symlink. ## Sockets, FIFOs, devices, and other special filesystem objects return `Bool.False`. - is_sym_link! : Path => Try(Bool, [PathErr(IOErr, Path), ..]) + is_sym_link! : Path => Try(Bool, [PathErr(IOErr, Path)]) is_sym_link! = |path| match type!(path) { Ok(IsSymLink) => Ok(Bool.True) @@ -79,7 +79,7 @@ Path := [ } ## Returns `True` if the path exists on disk. - exists! : Path => Try(Bool, [PathErr(IOErr, Path), ..]) + exists! : Path => Try(Bool, [PathErr(IOErr, Path)]) exists! = |path| match type!(path) { Ok(_) => Ok(Bool.True) @@ -92,7 +92,7 @@ Path := [ ## `IsOther` represents sockets, FIFOs, block and character devices, and any ## platform-specific object that is not a regular file, directory, or symbolic ## link. On Windows this includes unrecognized reparse-point types. - type! : Path => Try([IsFile, IsDir, IsSymLink, IsOther], [PathErr(IOErr, Path), ..]) + type! : Path => Try([IsFile, IsDir, IsSymLink, IsOther], [PathErr(IOErr, Path)]) type! = |path| { Host.path_type!(to_raw(path)) .map_err(|err| PathErr(err, path)) @@ -100,16 +100,16 @@ Path := [ } ## Make a path absolute without requiring it to exist. This does not resolve symlinks. - absolute! : Path => Try(Path, [PathErr(IOErr, Path), ..]) + absolute! : Path => Try(Path, [PathErr(IOErr, Path)]) absolute! = |path| Host.path_absolute!(to_raw(path)).map_ok(from_raw).map_err(|err| PathErr(err, path)) ## Resolve an existing path, including symbolic links, to its absolute native path. - canonicalize! : Path => Try(Path, [PathErr(IOErr, Path), ..]) + canonicalize! : Path => Try(Path, [PathErr(IOErr, Path)]) canonicalize! = |path| Host.path_canonicalize!(to_raw(path)).map_ok(from_raw).map_err(|err| PathErr(err, path)) ## Copy a regular file and its permissions, replacing the destination file. ## Rejects same-file copies (including hard-link aliases) and special files. - copy! : Path, Path => Try({}, [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr }), ..]) + copy! : Path, Path => Try({}, [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr })]) copy! = |source, destination| Host.path_copy!(to_raw(source), to_raw(destination)).map_err(copy_error) @@ -117,29 +117,29 @@ Path := [ ## Missing destination parent directories are created. ## Permissions are preserved; timestamps, ownership, ACLs and other metadata are not. ## Errors may leave a partial destination. Cycles and overlapping source/destination trees are rejected. - copy_dir! : Path, Path => Try({}, [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr }), ..]) + copy_dir! : Path, Path => Try({}, [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr })]) copy_dir! = |source, destination| copy_dir_with!(source, destination, { symlinks: Follow, destination: RequireNew }) ## Choose whether to preserve links and whether to merge an existing destination tree. ## Merge replaces regular files. Existing destination directory symlinks are rejected. - copy_dir_with! : Path, Path, { symlinks : [Follow, Preserve], destination : [RequireNew, Merge] } => Try({}, [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr }), ..]) + copy_dir_with! : Path, Path, { symlinks : [Follow, Preserve], destination : [RequireNew, Merge] } => Try({}, [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr })]) copy_dir_with! = |source, destination, options| Host.path_copy_dir!(to_raw(source), to_raw(destination), options).map_err(copy_error) ## Read all bytes from a file at this path. - read_bytes! : Path => Try(List(U8), [PathErr(IOErr, Path), ..]) + read_bytes! : Path => Try(List(U8), [PathErr(IOErr, Path)]) read_bytes! = |path| map_file_result(Host.file_read_bytes!(to_raw(path)), path) ## Write bytes to a file at this path, replacing any existing contents. - write_bytes! : Path, List(U8) => Try({}, [PathErr(IOErr, Path), ..]) + write_bytes! : Path, List(U8) => Try({}, [PathErr(IOErr, Path)]) write_bytes! = |path, bytes| map_file_result(Host.file_write_bytes!(to_raw(path), bytes), path) ## Read a UTF-8 file at this path. - read_utf8! : Path => Try(Str, [PathErr(IOErr, Path), ..]) + read_utf8! : Path => Try(Str, [PathErr(IOErr, Path)]) read_utf8! = |path| map_file_result(Host.file_read_utf8!(to_raw(path)), path) ## Write a UTF-8 file at this path, replacing any existing contents. - write_utf8! : Path, Str => Try({}, [PathErr(IOErr, Path), ..]) + write_utf8! : Path, Str => Try({}, [PathErr(IOErr, Path)]) write_utf8! = |path, content| map_file_result(Host.file_write_utf8!(to_raw(path), content), path) ## Replace every occurrence of `pattern` with `replacement` in the UTF-8 file @@ -148,49 +148,49 @@ Path := [ ## This reads the whole file, substitutes, and writes it back, so it is not ## atomic: a failure mid-write can leave the file partially written, exactly ## as a bare [write_utf8!] would. - replace_utf8! : Path, Str, Str => Try({}, [PathErr(IOErr, Path), ..]) + replace_utf8! : Path, Str, Str => Try({}, [PathErr(IOErr, Path)]) replace_utf8! = |path, pattern, replacement| { content = read_utf8!(path)? write_utf8!(path, Str.replace_each(content, pattern, replacement)) } ## Delete a file at this path. - delete! : Path => Try({}, [PathErr(IOErr, Path), ..]) + delete! : Path => Try({}, [PathErr(IOErr, Path)]) delete! = |path| map_file_result(Host.file_delete!(to_raw(path)), path) ## Return the size of the file at this path in bytes. - size_in_bytes! : Path => Try(U64, [PathErr(IOErr, Path), ..]) + size_in_bytes! : Path => Try(U64, [PathErr(IOErr, Path)]) size_in_bytes! = |path| map_file_result(Host.file_size_in_bytes!(to_raw(path)), path) ## Check whether the file at this path has any executable bit set. - is_executable! : Path => Try(Bool, [PathErr(IOErr, Path), ..]) + is_executable! : Path => Try(Bool, [PathErr(IOErr, Path)]) is_executable! = |path| map_file_result(Host.file_is_executable!(to_raw(path)), path) ## Check whether the file at this path has a readable owner permission bit set. - is_readable! : Path => Try(Bool, [PathErr(IOErr, Path), ..]) + is_readable! : Path => Try(Bool, [PathErr(IOErr, Path)]) is_readable! = |path| map_file_result(Host.file_is_readable!(to_raw(path)), path) ## Check whether the file at this path has a writable owner permission bit set. - is_writable! : Path => Try(Bool, [PathErr(IOErr, Path), ..]) + is_writable! : Path => Try(Bool, [PathErr(IOErr, Path)]) is_writable! = |path| map_file_result(Host.file_is_writable!(to_raw(path)), path) ## Return the last accessed time as nanoseconds since the Unix epoch. - time_accessed! : Path => Try(U128, [PathErr(IOErr, Path), ..]) + time_accessed! : Path => Try(U128, [PathErr(IOErr, Path)]) time_accessed! = |path| map_file_result(Host.file_time_accessed!(to_raw(path)), path) ## Return the last modified time as nanoseconds since the Unix epoch. - time_modified! : Path => Try(U128, [PathErr(IOErr, Path), ..]) + time_modified! : Path => Try(U128, [PathErr(IOErr, Path)]) time_modified! = |path| map_file_result(Host.file_time_modified!(to_raw(path)), path) ## Return the creation time as nanoseconds since the Unix epoch. - time_created! : Path => Try(U128, [PathErr(IOErr, Path), ..]) + time_created! : Path => Try(U128, [PathErr(IOErr, Path)]) time_created! = |path| map_file_result(Host.file_time_created!(to_raw(path)), path) ## Create a hard link at `link` pointing to `original`. ## ## Errors report `original`, which is what a missing-file failure refers to. ## Some failures concern `link` instead, such as `AlreadyExists`. - hard_link! : Path, Path => Try({}, [PathErr(IOErr, Path), ..]) + hard_link! : Path, Path => Try({}, [PathErr(IOErr, Path)]) hard_link! = |original, link| map_file_result(Host.file_hard_link!(to_raw(original), to_raw(link)), original) @@ -198,28 +198,28 @@ Path := [ ## ## Errors report `from`; a failure may still concern `to`, such as its ## parent directory not existing. - rename! : Path, Path => Try({}, [PathErr(IOErr, Path), ..]) + rename! : Path, Path => Try({}, [PathErr(IOErr, Path)]) rename! = |from, to| map_file_result(Host.file_rename!(to_raw(from), to_raw(to)), from) ## Create a directory at this path. - create_dir! : Path => Try({}, [PathErr(IOErr, Path), ..]) + create_dir! : Path => Try({}, [PathErr(IOErr, Path)]) create_dir! = |path| map_dir_result(Host.dir_create!(to_raw(path)), path) ## Create a directory and any missing parent directories at this path. - create_all! : Path => Try({}, [PathErr(IOErr, Path), ..]) + create_all! : Path => Try({}, [PathErr(IOErr, Path)]) create_all! = |path| map_dir_result(Host.dir_create_all!(to_raw(path)), path) ## Delete an empty directory at this path. - delete_empty! : Path => Try({}, [PathErr(IOErr, Path), ..]) + delete_empty! : Path => Try({}, [PathErr(IOErr, Path)]) delete_empty! = |path| map_dir_result(Host.dir_delete_empty!(to_raw(path)), path) ## Delete a directory and all contents at this path. - delete_all! : Path => Try({}, [PathErr(IOErr, Path), ..]) + delete_all! : Path => Try({}, [PathErr(IOErr, Path)]) delete_all! = |path| map_dir_result(Host.dir_delete_all!(to_raw(path)), path) ## List the entries in the directory at this path. - list! : Path => Try(List(Path), [PathErr(IOErr, Path), ..]) + list! : Path => Try(List(Path), [PathErr(IOErr, Path)]) list! = |path| match Host.dir_list!(to_raw(path)) { Ok(paths) => Ok(paths.map(from_raw)) @@ -262,7 +262,7 @@ Path := [ windows_u16s = |list| Windows(list) ## Convert a path to a string if its raw representation is valid text. - to_str : Path -> Try(Str, [InvalidStr(U64), ..]) + to_str : Path -> Try(Str, [InvalidStr(U64)]) to_str = |path| match path { Utf8(str) => Ok(str) @@ -398,14 +398,14 @@ Path := [ } } -map_file_result : Try(a, [FileErr(IOErr)]), Path -> Try(a, [PathErr(IOErr, Path), ..]) +map_file_result : Try(a, [FileErr(IOErr)]), Path -> Try(a, [PathErr(IOErr, Path)]) map_file_result = |result, path| match result { Ok(value) => Ok(value) Err(FileErr(err)) => Err(PathErr(err, path)) } -map_dir_result : Try(a, [DirErr(IOErr)]), Path -> Try(a, [PathErr(IOErr, Path), ..]) +map_dir_result : Try(a, [DirErr(IOErr)]), Path -> Try(a, [PathErr(IOErr, Path)]) map_dir_result = |result, path| match result { Ok(value) => Ok(value) @@ -468,7 +468,7 @@ utf8_to_utf16 = |remaining, out| } } -utf16_to_str : List(U16) -> Try(Str, [InvalidStr(U64), ..]) +utf16_to_str : List(U16) -> Try(Str, [InvalidStr(U64)]) utf16_to_str = |u16s| match utf16_to_utf8(u16s, [], 0) { Ok(bytes) => @@ -768,5 +768,5 @@ expect { } } -copy_error : Host.CopyFailure -> [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr }), ..] +copy_error : Host.CopyFailure -> [CopyErr({ operation : Str, source : Path, destination : Path, error : IOErr })] copy_error = |failure| CopyErr({ operation: failure.operation, source: Path.from_raw(failure.source), destination: Path.from_raw(failure.destination), error: failure.error }) diff --git a/platform/Random.roc b/platform/Random.roc index 35ac33a0..98b621bd 100644 --- a/platform/Random.roc +++ b/platform/Random.roc @@ -5,15 +5,18 @@ import Host Random :: [].{ ## Generate a random 64-bit unsigned integer seed. - seed_u64! : () => Try(U64, [RandomErr(IOErr), ..]) + seed_u64! : () => Try(U64, [RandomErr(IOErr)]) seed_u64! = || widen_random_err(Host.random_seed_u64!()) ## Generate a random 32-bit unsigned integer seed. - seed_u32! : () => Try(U32, [RandomErr(IOErr), ..]) + seed_u32! : () => Try(U32, [RandomErr(IOErr)]) seed_u32! = || widen_random_err(Host.random_seed_u32!()) } -widen_random_err : Try(a, [RandomErr(IOErr)]) -> Try(a, [RandomErr(IOErr), ..]) +## Rebuild the error union so it is open at call sites. +## Passing a hosted function's result straight through leaves the union closed, +## which stops `?` from combining it with other error types. +widen_random_err : Try(v, [RandomErr(IOErr)]) -> Try(v, [RandomErr(IOErr)]) widen_random_err = |result| match result { Ok(value) => Ok(value) diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index 40d63b0a..18c04c8b 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -53,7 +53,7 @@ Sqlite :: [].{ to_inspect = |_| "Sqlite.Stmt()" ## Execute this prepared statement without returning rows. - execute! : Stmt, List(Binding) => Try({}, [RowsReturnedUseQueryInstead, SqliteErr(ErrCode, Str), ..]) + execute! : Stmt, List(Binding) => Try({}, [RowsReturnedUseQueryInstead, SqliteErr(ErrCode, Str)]) execute! = |stmt, bindings| { host_stmt = stmt_to_host(stmt) sqlite_bind!(host_stmt, bindings)? @@ -137,12 +137,12 @@ Sqlite :: [].{ DecodeErr : [NoSuchField(Str), SqliteErr(ErrCode, Str)] ## Prepare a `Stmt` for reuse by the prepared execute and query operations. - prepare! : { path : Path.Path, query : Str } => Try(Stmt, [SqliteErr(ErrCode, Str), ..]) + prepare! : { path : Path.Path, query : Str } => Try(Stmt, [SqliteErr(ErrCode, Str)]) prepare! = |{ path, query: q }| sqlite_prepare!(Path.to_raw(path), q).map_ok(|stmt| Stmt.{ host: stmt }) ## Execute a SQL statement that **doesn't return any rows** (INSERT/UPDATE/DELETE). - execute! : { path : Path.Path, query : Str, bindings : List(Binding) } => Try({}, [RowsReturnedUseQueryInstead, SqliteErr(ErrCode, Str), ..]) + execute! : { path : Path.Path, query : Str, bindings : List(Binding) } => Try({}, [RowsReturnedUseQueryInstead, SqliteErr(ErrCode, Str)]) execute! = |{ path, query: q, bindings }| { stmt = prepare!({ path, query: q })? stmt.execute!(bindings) @@ -347,7 +347,7 @@ sqlite_prepare! = |raw_path, query| Host.sqlite_prepare!(raw_path, query) .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) -sqlite_bind! : Host.SqliteStmt, List({ name : Str, value : [Null, Real(F64), Integer(I64), String(Str), Bytes(List(U8))] }) => Try({}, [SqliteErr(Sqlite.ErrCode, Str), ..]) +sqlite_bind! : Host.SqliteStmt, List({ name : Str, value : [Null, Real(F64), Integer(I64), String(Str), Bytes(List(U8))] }) => Try({}, [SqliteErr(Sqlite.ErrCode, Str)]) sqlite_bind! = |stmt, bindings| Host.sqlite_bind!(stmt, bindings) .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) @@ -358,7 +358,7 @@ sqlite_column_value! = |stmt, index| Host.sqlite_column_value!(stmt, index) .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) -sqlite_step! : Host.SqliteStmt => Try([Row, Done], [SqliteErr(Sqlite.ErrCode, Str), ..]) +sqlite_step! : Host.SqliteStmt => Try([Row, Done], [SqliteErr(Sqlite.ErrCode, Str)]) sqlite_step! = |stmt| match Host.sqlite_step!(stmt) { Ok(has_row) => if has_row { @@ -369,7 +369,7 @@ sqlite_step! = |stmt| Err({ code, message }) => Err(SqliteErr(code_from_i64(code), message)) } -sqlite_reset! : Host.SqliteStmt => Try({}, [SqliteErr(Sqlite.ErrCode, Str), ..]) +sqlite_reset! : Host.SqliteStmt => Try({}, [SqliteErr(Sqlite.ErrCode, Str)]) sqlite_reset! = |stmt| Host.sqlite_reset!(stmt) .map_err(|{ code, message }| SqliteErr(code_from_i64(code), message)) diff --git a/platform/Stderr.roc b/platform/Stderr.roc index 4addeed3..4a6de743 100644 --- a/platform/Stderr.roc +++ b/platform/Stderr.roc @@ -8,7 +8,7 @@ Stderr :: [].{ ## followed by a newline. ## ## > To write to `stderr` without the newline, see [Stderr.write!]. - line! : Str => Try({}, [StderrErr(IOErr), ..]) + line! : Str => Try({}, [StderrErr(IOErr)]) line! = |message| widen_stderr_err(Host.stderr_line!(message)) ## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). @@ -17,18 +17,21 @@ 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 => Try({}, [StderrErr(IOErr), ..]) + write! : Str => Try({}, [StderrErr(IOErr)]) write! = |message| widen_stderr_err(Host.stderr_write!(message)) ## 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), ..]) + write_bytes! : List(U8) => Try({}, [StderrErr(IOErr)]) write_bytes! = |bytes| widen_stderr_err(Host.stderr_write_bytes!(bytes)) } -widen_stderr_err : Try(a, [StderrErr(IOErr)]) -> Try(a, [StderrErr(IOErr), ..]) +## Rebuild the error union so it is open at call sites. +## Passing a hosted function's result straight through leaves the union closed, +## which stops `?` from combining it with other error types. +widen_stderr_err : Try(v, [StderrErr(IOErr)]) -> Try(v, [StderrErr(IOErr)]) widen_stderr_err = |result| match result { Ok(value) => Ok(value) diff --git a/platform/Stdin.roc b/platform/Stdin.roc index 0b547a8b..480c16ef 100644 --- a/platform/Stdin.roc +++ b/platform/Stdin.roc @@ -10,7 +10,7 @@ Stdin :: [].{ ## (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! : () => Try(Str, [EndOfFile, StdinErr(IOErr), ..]) + line! : () => Try(Str, [EndOfFile, StdinErr(IOErr)]) line! = || widen_stdin_eof_err(Host.stdin_line!()) ## Read bytes from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). @@ -19,16 +19,19 @@ Stdin :: [].{ ## > This is typically used in combination with [Tty.enable_raw_mode!], ## which disables default terminal behaviour and allows reading input ## without buffering until Enter key is pressed. - bytes! : () => Try(List(U8), [EndOfFile, StdinErr(IOErr), ..]) + bytes! : () => Try(List(U8), [EndOfFile, StdinErr(IOErr)]) bytes! = || widen_stdin_eof_err(Host.stdin_bytes!()) ## 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), ..]) + read_to_end! : () => Try(List(U8), [StdinErr(IOErr)]) read_to_end! = || widen_stdin_err(Host.stdin_read_to_end!()) } -widen_stdin_eof_err : Try(a, [EndOfFile, StdinErr(IOErr)]) -> Try(a, [EndOfFile, StdinErr(IOErr), ..]) +## Rebuild the error union so it is open at call sites. +## Passing a hosted function's result straight through leaves the union closed, +## which stops `?` from combining it with other error types. +widen_stdin_eof_err : Try(v, [EndOfFile, StdinErr(IOErr)]) -> Try(v, [EndOfFile, StdinErr(IOErr)]) widen_stdin_eof_err = |result| match result { Ok(value) => Ok(value) @@ -36,7 +39,7 @@ widen_stdin_eof_err = |result| Err(StdinErr(err)) => Err(StdinErr(err)) } -widen_stdin_err : Try(a, [StdinErr(IOErr)]) -> Try(a, [StdinErr(IOErr), ..]) +widen_stdin_err : Try(v, [StdinErr(IOErr)]) -> Try(v, [StdinErr(IOErr)]) widen_stdin_err = |result| match result { Ok(value) => Ok(value) diff --git a/platform/Stdout.roc b/platform/Stdout.roc index c8b48498..94dae48d 100644 --- a/platform/Stdout.roc +++ b/platform/Stdout.roc @@ -8,7 +8,7 @@ Stdout :: [].{ ## followed by a newline. ## ## > To write to `stdout` without the newline, see [Stdout.write!]. - line! : Str => Try({}, [StdoutErr(IOErr), ..]) + line! : Str => Try({}, [StdoutErr(IOErr)]) line! = |message| widen_stdout_err(Host.stdout_line!(message)) ## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). @@ -17,18 +17,21 @@ 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 => Try({}, [StdoutErr(IOErr), ..]) + write! : Str => Try({}, [StdoutErr(IOErr)]) write! = |message| widen_stdout_err(Host.stdout_write!(message)) ## 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), ..]) + write_bytes! : List(U8) => Try({}, [StdoutErr(IOErr)]) write_bytes! = |bytes| widen_stdout_err(Host.stdout_write_bytes!(bytes)) } -widen_stdout_err : Try(a, [StdoutErr(IOErr)]) -> Try(a, [StdoutErr(IOErr), ..]) +## Rebuild the error union so it is open at call sites. +## Passing a hosted function's result straight through leaves the union closed, +## which stops `?` from combining it with other error types. +widen_stdout_err : Try(v, [StdoutErr(IOErr)]) -> Try(v, [StdoutErr(IOErr)]) widen_stdout_err = |result| match result { Ok(value) => Ok(value) diff --git a/platform/Url.roc b/platform/Url.roc index 54952d56..54c612bb 100644 --- a/platform/Url.roc +++ b/platform/Url.roc @@ -370,7 +370,7 @@ validate_host = |raw_host| { } } -validate_dns_name : Str -> Try(Str, [InvalidHost(Str), ..]) +validate_dns_name : Str -> Try(Str, [InvalidHost(Str)]) validate_dns_name = |raw_host| { host = ascii_lower(raw_host) labels = Str.split_on(host, ".") @@ -394,7 +394,7 @@ validate_dns_name = |raw_host| { } } -validate_ipv4 : Str -> Try(Str, [InvalidIpv4(Str), ..]) +validate_ipv4 : Str -> Try(Str, [InvalidIpv4(Str)]) validate_ipv4 = |raw_host| { parts = Str.split_on(raw_host, ".") if List.len(parts) != 4 { @@ -423,7 +423,7 @@ parse_ipv4_parts = |parts, out| } } -parse_port : Str, [Http, Https] -> Try([None, Some(U16)], [InvalidPort(Str), PortOutOfRange(U64), ..]) +parse_port : Str, [Http, Https] -> Try([None, Some(U16)], [InvalidPort(Str), PortOutOfRange(U64)]) parse_port = |raw, scheme| match parse_decimal(raw) { Err(_) => Err(InvalidPort(raw)) @@ -462,7 +462,7 @@ parse_decimal = |raw| { # Addresses are validated as eight hexadecimal groups, with at most one :: # elision. IPv4-in-IPv6 syntax is outside this module's deliberately small # subset. Serialization expands elided groups and removes leading zeroes. -validate_ipv6 : Str -> Try(Str, [InvalidIpv6(Str), ..]) +validate_ipv6 : Str -> Try(Str, [InvalidIpv6(Str)]) validate_ipv6 = |raw| { pieces = Str.split_on(raw, "::") if List.len(pieces) > 2 { @@ -487,7 +487,7 @@ validate_ipv6 = |raw| { } } -parse_ipv6_side : Str -> Try(List(U16), [InvalidIpv6(Str), ..]) +parse_ipv6_side : Str -> Try(List(U16), [InvalidIpv6(Str)]) parse_ipv6_side = |raw| if Str.is_empty(raw) { Ok([]) @@ -495,7 +495,7 @@ parse_ipv6_side = |raw| parse_hex_groups(Str.split_on(raw, ":"), []) } -parse_hex_groups : List(Str), List(U16) -> Try(List(U16), [InvalidIpv6(Str), ..]) +parse_hex_groups : List(Str), List(U16) -> Try(List(U16), [InvalidIpv6(Str)]) parse_hex_groups = |parts, out| match parts { [] => Ok(out) diff --git a/tests/resource-lifetimes/resource-lifetime.roc b/tests/resource-lifetimes/resource-lifetime.roc index d7101c8b..ee6d1ffd 100644 --- a/tests/resource-lifetimes/resource-lifetime.roc +++ b/tests/resource-lifetimes/resource-lifetime.roc @@ -52,11 +52,11 @@ read_aliases! = |path, early| { if early { return Err(ExpectedEarlyReturn) } - var lines = [] + var $lines = [] for alias in aliases { - lines = List.append(lines, File.Reader.read_line!(alias)?) + $lines = List.append($lines, File.Reader.read_line!(alias)?) } - expect lines == [Str.to_utf8("first\n"), Str.to_utf8("second\n")] + expect $lines == [Str.to_utf8("first\n"), Str.to_utf8("second\n")] Ok({}) }