From 64c938dbf04e04833b7058f125ebd2ec5aa74e67 Mon Sep 17 00:00:00 2001 From: Anton-4-bot <309861636+Anton-4-bot@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:37:49 +0200 Subject: [PATCH] Update to Roc nightly-2026-10-03-c507926 Adapt the platform and its tooling to the compiler changes in this nightly: - Remove redundant type exposes such as `import IOErr exposing [IOErr]`, which are now an error: `redundant expose ... The type IOErr is automatically exposed when importing a type module.` - List main.roc first when bundling. `roc bundle` treats the first .roc file as the entry point and now reports its diagnostics, so the previous alphabetical order made Cmd.roc the entry and failed with `reference has no value` and `This Header type is declared to be in http.Header, which does not exist.` - Bundling the platform entry point now checks that every declared target input exists (`The targets section declares file libhost.a for target x64mac but this file doesn't exist.`). Add `--stub-missing-targets` to scripts/bundle.py, used only by test.py, so test bundles that only build the native target still work. Release bundles still require every real target. - Apply the new formatter output to platform and examples. - Bump the nightly pin in the CI workflows. The Nix pins stay on 2026-09-26 until roc-overlay publishes this nightly. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 6 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/release.yml | 8 +- examples/check-command.roc | 1 - examples/command-line-args.roc | 1 - examples/error-handling.roc | 1 - .../file-accessed-modified-created-time.roc | 1 - examples/file-permissions.roc | 1 - examples/file-read-buffered.roc | 1 - examples/file-read-write.roc | 1 - examples/file-replace.roc | 1 - examples/file-size.roc | 1 - examples/hello.roc | 1 - examples/http-client.roc | 2 +- examples/http-simple.roc | 3 +- examples/locale.roc | 1 - examples/path.roc | 2 +- examples/sqlite-basic.roc | 2 +- examples/sqlite-everything.roc | 1 - examples/tcp-client.roc | 1 - examples/temp-dir.roc | 1 - examples/time.roc | 1 - platform/Cmd.roc | 14 +-- platform/Env.roc | 4 +- platform/File.roc | 4 +- platform/Host.roc | 2 +- platform/InternalSqlite.roc | 38 +++---- platform/OsStr.roc | 6 +- platform/Path.roc | 12 +-- platform/Random.roc | 2 +- platform/Sqlite.roc | 2 +- platform/Stderr.roc | 2 +- platform/Stdin.roc | 2 +- platform/Stdout.roc | 2 +- platform/Url.roc | 78 +++++++------- platform/main.roc | 100 +++++++++--------- scripts/bundle.py | 50 ++++++++- scripts/test.py | 2 +- 38 files changed, 195 insertions(+), 165 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4279c3d2..b852017d 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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - 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 @@ -77,7 +77,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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - name: Install Rust toolchain shell: bash run: | @@ -122,7 +122,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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - name: Install Rust toolchain shell: bash run: | diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 43ae8f04..20e12687 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -40,7 +40,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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - name: Restore released docs env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 210d5b4d..2fc1d653 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,7 +114,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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - name: Install Zig uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f @@ -218,7 +218,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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - name: Install Rust shell: bash @@ -263,7 +263,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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - name: Create docs staging tree run: mkdir -p docs-site @@ -435,7 +435,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-26-d6267b4 + nightly-tag: nightly-2026-10-03-c507926 - name: Download release metadata uses: actions/download-artifact@v8 diff --git a/examples/check-command.roc b/examples/check-command.roc index 8d9e068a..89562b77 100644 --- a/examples/check-command.roc +++ b/examples/check-command.roc @@ -13,7 +13,6 @@ report! = |name| { main! : List(OsStr) => Try({}, _) main! = |_args| { - # A command found on the PATH. report!("sh")? diff --git a/examples/command-line-args.roc b/examples/command-line-args.roc index 3e1bff0f..1c6e2b0a 100644 --- a/examples/command-line-args.roc +++ b/examples/command-line-args.roc @@ -13,7 +13,6 @@ main! = |args| { match args { [first_arg, ..] => { - Stdout.line!("received argument: ${OsStr.display(first_arg)}")? match OsStr.to_raw(first_arg) { diff --git a/examples/error-handling.roc b/examples/error-handling.roc index 8580be2a..bc1f1a48 100644 --- a/examples/error-handling.roc +++ b/examples/error-handling.roc @@ -8,7 +8,6 @@ import pf.Path main! : List(OsStr) => Try({}, _) main! = |_args| { - file_name : Path file_name = "test-file.txt" diff --git a/examples/file-accessed-modified-created-time.roc b/examples/file-accessed-modified-created-time.roc index 0c551069..1e72d294 100644 --- a/examples/file-accessed-modified-created-time.roc +++ b/examples/file-accessed-modified-created-time.roc @@ -8,7 +8,6 @@ import pf.Utc main! : List(OsStr) => Try({}, _) main! = |args| { - file : Path file = path_argument(args)? diff --git a/examples/file-permissions.roc b/examples/file-permissions.roc index da36d88b..bad1ec98 100644 --- a/examples/file-permissions.roc +++ b/examples/file-permissions.roc @@ -7,7 +7,6 @@ import pf.Path main! : List(OsStr) => Try({}, _) main! = |args| { - file : Path file = path_argument(args)? diff --git a/examples/file-read-buffered.roc b/examples/file-read-buffered.roc index 5b49afdc..965924da 100644 --- a/examples/file-read-buffered.roc +++ b/examples/file-read-buffered.roc @@ -12,7 +12,6 @@ import pf.Path main! : List(OsStr) => Try({}, _) main! = |_args| { - reader : File.Reader reader = File.open_reader!("LICENSE")? diff --git a/examples/file-read-write.roc b/examples/file-read-write.roc index ae3ba5bc..d79f826c 100644 --- a/examples/file-read-write.roc +++ b/examples/file-read-write.roc @@ -7,7 +7,6 @@ import pf.Path main! : List(OsStr) => Try({}, _) main! = |_args| { - out_file : Path out_file = "out.txt" diff --git a/examples/file-replace.roc b/examples/file-replace.roc index 759fa9d5..e78a114d 100644 --- a/examples/file-replace.roc +++ b/examples/file-replace.roc @@ -7,7 +7,6 @@ import pf.Path main! : List(OsStr) => Try({}, _) main! = |_args| { - file : Path file = "greeting.txt" diff --git a/examples/file-size.roc b/examples/file-size.roc index 5844d29e..3a0cef5d 100644 --- a/examples/file-size.roc +++ b/examples/file-size.roc @@ -7,7 +7,6 @@ import pf.Path main! : List(OsStr) => Try({}, _) main! = |args| { - file : Path file = path_argument(args)? diff --git a/examples/hello.roc b/examples/hello.roc index eb4fab98..2e2d714d 100644 --- a/examples/hello.roc +++ b/examples/hello.roc @@ -6,7 +6,6 @@ import pf.Stdout main! : List(OsStr) => Try({}, _) main! = |args| { - name : Str name = greeting_name(args) diff --git a/examples/http-client.roc b/examples/http-client.roc index 33b97a70..9ebd5bfd 100644 --- a/examples/http-client.roc +++ b/examples/http-client.roc @@ -1,7 +1,7 @@ ## Send HTTP requests and handle UTF-8, JSON, and transport failures. app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.23.0/GNN5tt2gKdX4dhawg4915C4YB193woHFdcCkz31fhGxv.tar.zst", http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst", + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.23.0/GNN5tt2gKdX4dhawg4915C4YB193woHFdcCkz31fhGxv.tar.zst", } import pf.OsStr diff --git a/examples/http-simple.roc b/examples/http-simple.roc index a531a83a..3d4b1913 100644 --- a/examples/http-simple.roc +++ b/examples/http-simple.roc @@ -1,7 +1,7 @@ ## Fetch UTF-8, JSON, and HTML responses from a local HTTP server. app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.23.0/GNN5tt2gKdX4dhawg4915C4YB193woHFdcCkz31fhGxv.tar.zst", http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst", + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.23.0/GNN5tt2gKdX4dhawg4915C4YB193woHFdcCkz31fhGxv.tar.zst", } import pf.OsStr @@ -11,7 +11,6 @@ import http.Request main! : List(OsStr) => Try({}, _) main! = |_args| { - hello_str = Http.get_utf8!("http://127.0.0.1:9000/utf8test") ? |err| GetUtf8Failed(err) Stdout.line!("I received '${hello_str}' from the server.")? diff --git a/examples/locale.roc b/examples/locale.roc index 9537135e..1c0017df 100644 --- a/examples/locale.roc +++ b/examples/locale.roc @@ -7,7 +7,6 @@ import pf.Locale main! : List(OsStr) => Try({}, _) main! = |_args| { - example_locale : Locale example_locale = "en-US" diff --git a/examples/path.roc b/examples/path.roc index 0b45ea0e..a8c821f5 100644 --- a/examples/path.roc +++ b/examples/path.roc @@ -1,7 +1,7 @@ ## Inspect a path's filename, extension, string representation, and type (file/dir/symlink). app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.23.0/GNN5tt2gKdX4dhawg4915C4YB193woHFdcCkz31fhGxv.tar.zst" } -import pf.OsStr exposing [OsStr] +import pf.OsStr import pf.Stdout import pf.Path diff --git a/examples/sqlite-basic.roc b/examples/sqlite-basic.roc index 579f11fc..ec8fd4f4 100644 --- a/examples/sqlite-basic.roc +++ b/examples/sqlite-basic.roc @@ -50,7 +50,7 @@ print_line! : Str => Try({}, _) print_line! = |line| Stdout.line!(line) query_todos_by_status! = |db_path, status| -# `many` when you expect multiple rows to be returned. + # `many` when you expect multiple rows to be returned. Sqlite.query_many!({ path: db_path, query: "SELECT id, task, status FROM todos WHERE status = :status;", diff --git a/examples/sqlite-everything.roc b/examples/sqlite-everything.roc index 9fb18b37..128178b3 100644 --- a/examples/sqlite-everything.roc +++ b/examples/sqlite-everything.roc @@ -23,7 +23,6 @@ main! = |_args| run!() run! : () => Try({}, _) run! = || { - # Read from environment variable, or use default db_path = match Env.var!("DB_PATH") { Ok(p) => Path.from_os_str(p) diff --git a/examples/tcp-client.roc b/examples/tcp-client.roc index 0ebe3d16..ab0d737b 100644 --- a/examples/tcp-client.roc +++ b/examples/tcp-client.roc @@ -13,7 +13,6 @@ import pf.Stdin # then run this example. main! : List(OsStr) => Try({}, _) main! = |_args| { - stream : Tcp.Stream stream = Tcp.connect!("127.0.0.1", 8085, 1_000) ? |err| ConnectFailed(err) diff --git a/examples/temp-dir.roc b/examples/temp-dir.roc index 7fadc855..f2829acb 100644 --- a/examples/temp-dir.roc +++ b/examples/temp-dir.roc @@ -8,7 +8,6 @@ import pf.Path main! : List(OsStr) => Try({}, _) main! = |_args| { - temp_dir_path : Path temp_dir_path = Env.temp_dir!() diff --git a/examples/time.roc b/examples/time.roc index 7f791925..4e7f24fe 100644 --- a/examples/time.roc +++ b/examples/time.roc @@ -8,7 +8,6 @@ import pf.Sleep main! : List(OsStr) => Try({}, _) main! = |_args| { - start : U128 start = Utc.now!() diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 90bfae19..60885eed 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -1,8 +1,8 @@ -import IOErr exposing [IOErr] +import IOErr import Host -import OsStr exposing [OsStr] +import OsStr import Env -import Path exposing [Path] +import Path ## Build and run child processes with native-safe programs, arguments, and ## environment values. @@ -453,7 +453,7 @@ flatten_arg_pairs = |pairs, acc, idx| { flatten_arg_pairs(pairs, acc.append(pair.0).append(pair.1), idx + 1) Err(_) => acc - } + } } } @@ -508,7 +508,7 @@ path_dirs = |path_value, is_windows| split_on(u16s, if is_windows ';' else ':') .keep_if(|segment| !List.is_empty(segment)) .map(Path.windows_u16s) - } + } ## Split a list into segments on a separator element (segments may be empty). split_on : List(a), a -> List(List(a)) where [a.is_eq : a, a -> Bool] @@ -543,7 +543,7 @@ search_dirs! = |dirs, command, extensions, is_windows| } else { search_dirs!(rest, command, extensions, is_windows) } - } + } search_extensions! : Path, Str, List(Str), Bool => Bool search_extensions! = |dir, command, extensions, is_windows| @@ -555,7 +555,7 @@ search_extensions! = |dir, command, extensions, is_windows| } else { search_extensions!(dir, command, rest, is_windows) } - } + } ## Whether a specific candidate path is runnable: on Windows it must exist, on ## Unix it must carry an executable bit. Either way a directory is rejected, diff --git a/platform/Env.roc b/platform/Env.roc index 4c18076b..25e0889c 100644 --- a/platform/Env.roc +++ b/platform/Env.roc @@ -1,6 +1,6 @@ import Host -import IOErr exposing [IOErr] -import OsStr exposing [OsStr] +import IOErr +import OsStr import Path ## Read and modify the process environment without losing native OS strings. diff --git a/platform/File.roc b/platform/File.roc index 18023147..3869f076 100644 --- a/platform/File.roc +++ b/platform/File.roc @@ -1,6 +1,6 @@ import Host import Path -import IOErr exposing [IOErr] +import IOErr ## Read file bytes incrementally and seek within files. ## @@ -93,7 +93,7 @@ File :: [].{ Ok(bytes) => Ok((Ok(bytes), Reading(current))) Err(FileErr(err)) => Ok((Err(FileErr(err)), Finished)) } - }, + }, ), ) } diff --git a/platform/Host.roc b/platform/Host.roc index 961a53b2..adc0bf7a 100644 --- a/platform/Host.roc +++ b/platform/Host.roc @@ -1,4 +1,4 @@ -import IOErr exposing [IOErr] +import IOErr import InternalHttp import InternalSqlite diff --git a/platform/InternalSqlite.roc b/platform/InternalSqlite.roc index b792d597..16cdd144 100644 --- a/platform/InternalSqlite.roc +++ b/platform/InternalSqlite.roc @@ -1,26 +1,26 @@ ## Define the host-ABI types shared by SQLite effects and the public module. ## These records map directly to the generated Rust glue types. InternalSqlite :: [].{ - SqliteError : { - code : I64, - message : Str, - } + SqliteError : { + code : I64, + message : Str, + } - SqliteValue : [ - Null, - Real(F64), - Integer(I64), - String(Str), - Bytes(List(U8)), - ] + SqliteValue : [ + Null, + Real(F64), + Integer(I64), + String(Str), + Bytes(List(U8)), + ] - SqliteState : [ - Row, - Done, - ] + SqliteState : [ + Row, + Done, + ] - SqliteBindings : { - name : Str, - value : SqliteValue, - } + SqliteBindings : { + name : Str, + value : SqliteValue, + } } diff --git a/platform/OsStr.roc b/platform/OsStr.roc index 36a8ff36..3a4646b1 100644 --- a/platform/OsStr.roc +++ b/platform/OsStr.roc @@ -100,7 +100,7 @@ OsStr := [ Ok(str) => "OsStr.windows(${Json.to_str(str)})" Err(_) => "OsStr.windows_u16s(${Str.inspect(u16s)})" } - } + } ## Compare OS strings by their exact tagged representation. is_eq : OsStr, OsStr -> Bool @@ -195,7 +195,7 @@ utf16_to_utf8 = |remaining, out, index| [unit, ..] if is_surrogate(unit) => Err(InvalidUtf16(index)) [unit, .. as rest] => utf16_to_utf8(rest, append_code_point_utf8(out, U16.to_u32(unit)), index + 1) - } + } utf16_to_utf8_lossy : List(U16) -> List(U8) utf16_to_utf8_lossy = |u16s| utf16_to_utf8_lossy_help(u16s, []) @@ -214,7 +214,7 @@ utf16_to_utf8_lossy_help = |remaining, out| utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, 0xFFFD)) [unit, .. as rest] => utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, U16.to_u32(unit))) - } + } append_code_point_utf8 : List(U8), U32 -> List(U8) append_code_point_utf8 = |out, code_point| diff --git a/platform/Path.roc b/platform/Path.roc index d248368d..fbd4dd85 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -1,6 +1,6 @@ -import IOErr exposing [IOErr] +import IOErr import Host -import OsStr exposing [OsStr] +import OsStr path_type_from_host : Host.PathType -> [IsFile, IsDir, IsSymLink, IsOther] path_type_from_host = |path_type| @@ -301,7 +301,7 @@ Path := [ Ok(str) => "Path.windows(${Json.to_str(str)})" Err(_) => "Path.windows_u16s(${Str.inspect(u16s)})" } - } + } ## Compare paths by their exact tagged representation. is_eq : Path, Path -> Bool @@ -358,7 +358,7 @@ Path := [ Err(NotFound) => Ok(path) } } - } + } ## Returns the filename extension without the leading dot. ext : Path -> Try(Path, [IsDirPath, EndsInDots]) @@ -497,7 +497,7 @@ utf16_to_utf8 = |remaining, out, index| [unit, .. as rest] => utf16_to_utf8(rest, append_code_point_utf8(out, U16.to_u32(unit)), index + 1) - } + } utf16_to_utf8_lossy : List(U16) -> List(U8) utf16_to_utf8_lossy = |u16s| utf16_to_utf8_lossy_help(u16s, []) @@ -520,7 +520,7 @@ utf16_to_utf8_lossy_help = |remaining, out| [unit, .. as rest] => utf16_to_utf8_lossy_help(rest, append_code_point_utf8(out, U16.to_u32(unit))) - } + } append_code_point_utf8 : List(U8), U32 -> List(U8) append_code_point_utf8 = |out, code_point| diff --git a/platform/Random.roc b/platform/Random.roc index 98b621bd..e71ca8d3 100644 --- a/platform/Random.roc +++ b/platform/Random.roc @@ -1,4 +1,4 @@ -import IOErr exposing [IOErr] +import IOErr import Host ## Obtain random seed values from the operating system's entropy source. diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index 3600f04e..3dc8a2a0 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -405,7 +405,7 @@ decode_rows! = |stmt, gen_decode| { Err(e) => Err(e) Ok(row) => helper!(out.append(row)) } - } + } helper!([]) } diff --git a/platform/Stderr.roc b/platform/Stderr.roc index 4a6de743..ffda7f30 100644 --- a/platform/Stderr.roc +++ b/platform/Stderr.roc @@ -1,4 +1,4 @@ -import IOErr exposing [IOErr] +import IOErr import Host ## Write text or raw bytes to the process's standard error stream. diff --git a/platform/Stdin.roc b/platform/Stdin.roc index 480c16ef..260a8eac 100644 --- a/platform/Stdin.roc +++ b/platform/Stdin.roc @@ -1,4 +1,4 @@ -import IOErr exposing [IOErr] +import IOErr import Host ## Read lines, chunks, or all remaining bytes from standard input. diff --git a/platform/Stdout.roc b/platform/Stdout.roc index 94dae48d..3ebd5994 100644 --- a/platform/Stdout.roc +++ b/platform/Stdout.roc @@ -1,4 +1,4 @@ -import IOErr exposing [IOErr] +import IOErr import Host ## Write text or raw bytes to the process's standard output stream. diff --git a/platform/Url.roc b/platform/Url.roc index 54c612bb..29398ef2 100644 --- a/platform/Url.roc +++ b/platform/Url.roc @@ -165,7 +165,7 @@ Url :: { append_path_segments : Url, List(Str) -> Url append_path_segments = |url, segments| { suffix = Str.join_with(segments.map(percent_encode), "/") - next_path = + next_path = if Str.is_empty(suffix) { url.path } else if url.path == "/" { @@ -192,7 +192,7 @@ Url :: { append_query_param : Url, Str, Str -> Url append_query_param = |url, key, value| { pair = Str.concat(Str.concat(form_encode(key), "="), form_encode(value)) - next_query = + next_query = match url.query { None => pair Some("") => pair @@ -225,7 +225,7 @@ Url :: { NotFound => (form_decode(pair), "") }, ) - } + } ## Replace or remove the query. ## @@ -233,7 +233,7 @@ Url :: { ## Unicode but must otherwise already obey URL query syntax. with_query : Url, [None, Some(Str)] -> Try(Url, ParseErr) with_query = |url, option| { - next_query_option = + next_query_option = match option { None => Ok(None) Some(raw) => @@ -241,7 +241,7 @@ Url :: { Ok(value) => Ok(Some(value)) Err(err) => Err(err) } - }? + }? Ok( Url.{ scheme: url.scheme, @@ -259,7 +259,7 @@ Url :: { ## Some("") produces a present empty fragment. Unicode is percent-encoded. with_fragment : Url, [None, Some(Str)] -> Try(Url, ParseErr) with_fragment = |url, option| { - next_fragment_option = + next_fragment_option = match option { None => Ok(None) Some(raw) => @@ -267,7 +267,7 @@ Url :: { Ok(value) => Ok(Some(value)) Err(err) => Err(err) } - }? + }? Ok( Url.{ scheme: url.scheme, @@ -285,7 +285,7 @@ Url :: { parse_absolute : Str -> Try(Url, Url.ParseErr) parse_absolute = |input| { - scheme_parts = + scheme_parts = match split_first(input, "://") { Found(parts) => Ok(parts) NotFound => @@ -294,8 +294,8 @@ parse_absolute = |input| { } else { Err(MissingScheme) } - }? - scheme = + }? + scheme = match ascii_lower(scheme_parts.before) { "http" => Ok(Http) "https" => Ok(Https) @@ -330,7 +330,7 @@ parse_authority = |authority, scheme| { Found({ before, after }) => { raw_ipv6 = drop_prefix(before, "[") host = validate_ipv6(raw_ipv6)? - port = + port = if Str.is_empty(after) { Ok(None) } else if starts_with(after, ":") { @@ -342,13 +342,13 @@ parse_authority = |authority, scheme| { } } } else { - { raw_host, raw_port } = + { raw_host, raw_port } = match split_last(authority, ":") { Found({ before, after }) => { raw_host: before, raw_port: Some(after) } NotFound => { raw_host: authority, raw_port: None } } host = validate_host(raw_host)? - port = + port = match raw_port { None => Ok(None) Some(raw) => parse_port(raw, scheme) @@ -374,7 +374,7 @@ validate_dns_name : Str -> Try(Str, [InvalidHost(Str)]) validate_dns_name = |raw_host| { host = ascii_lower(raw_host) labels = Str.split_on(host, ".") - valid = + valid = List.len(Str.to_utf8(host)) <= 253 and List.all( labels, @@ -421,7 +421,7 @@ parse_ipv4_parts = |parts, out| } Err(_) => Err(BadIpv4Part) } - } + } parse_port : Str, [Http, Https] -> Try([None, Some(U16)], [InvalidPort(Str), PortOutOfRange(U64)]) parse_port = |raw, scheme| @@ -432,7 +432,7 @@ parse_port = |raw, scheme| Err(PortOutOfRange(value)) } else { port = U64.to_u16_wrap(value) - is_default = + is_default = match scheme { Http => port == 80 Https => port == 443 @@ -445,7 +445,7 @@ parse_port = |raw, scheme| }, ) } - } + } parse_decimal : Str -> Try(U64, [NotDecimal]) parse_decimal = |raw| { @@ -534,12 +534,12 @@ u16_to_hex_help = |value, digits| { parse_suffix : Str -> Try({ fragment : [None, Some(Str)], path : Str, query : [None, Some(Str)] }, Url.ParseErr) parse_suffix = |suffix| { - { before_fragment, fragment } = + { before_fragment, fragment } = match split_first(suffix, "#") { Found({ before, after }) => { before_fragment: before, fragment: Some(after) } NotFound => { before_fragment: suffix, fragment: None } } - { raw_path, query } = + { raw_path, query } = match split_first(before_fragment, "?") { Found({ before, after }) => { raw_path: before, query: Some(after) } NotFound => { raw_path: before_fragment, query: None } @@ -568,7 +568,7 @@ validate_optional = |option, kind| Ok(value) => Ok(Some(value)) Err(err) => Err(err) } - } + } validate_component : Str, [Fragment, Path, Query] -> Try(Str, Url.ParseErr) validate_component = |raw, kind| @@ -627,7 +627,7 @@ resolve_reference = |base, reference| { Err(MissingScheme) } else { relative = parse_relative(reference)? - next_path = + next_path = if Str.is_empty(relative.path) { base.path } else if starts_with(relative.path, "/") { @@ -635,7 +635,7 @@ resolve_reference = |base, reference| { } else { normalize_path(Str.concat(path_directory(base.path), relative.path)) } - next_query = + next_query = match relative.query { Some(value) => Some(value) None => if Str.is_empty(relative.path) { @@ -659,12 +659,12 @@ resolve_reference = |base, reference| { parse_relative : Str -> Try({ fragment : [None, Some(Str)], path : Str, query : [None, Some(Str)] }, Url.ParseErr) parse_relative = |reference| { - { before_fragment, fragment } = + { before_fragment, fragment } = match split_first(reference, "#") { Found({ before, after }) => { before_fragment: before, fragment: Some(after) } NotFound => { before_fragment: reference, fragment: None } } - { raw_path, query } = + { raw_path, query } = match split_first(before_fragment, "?") { Found({ before, after }) => { raw_path: before, query: Some(after) } NotFound => { raw_path: before_fragment, query: None } @@ -714,22 +714,22 @@ path_directory = |path_str| { serialize : Url, Bool -> Str serialize = |url, include_fragment| { - scheme_str = + scheme_str = match url.scheme { Http => "http" Https => "https" } - port_str = + port_str = match url.port { None => "" Some(value) => Str.concat(":", U16.to_str(value)) } - query_str = + query_str = match url.query { None => "" Some(value) => Str.concat("?", value) } - fragment_str = + fragment_str = if include_fragment { match url.fragment { None => "" @@ -1110,7 +1110,7 @@ expect Ok(resolved) => Url.to_str(resolved) == "https://example.com/c?new=2#fresh" Err(_) => False } - } + } expect match Url.parse("https://example.com/a/b?old=1#old") { @@ -1120,7 +1120,7 @@ expect Ok(resolved) => Url.to_str(resolved) == "https://example.com/a/b?new=2" Err(_) => False } - } + } expect match Url.parse("https://example.com/a/b?old=1") { @@ -1130,7 +1130,7 @@ expect Ok(resolved) => Url.to_str(resolved) == "https://example.com/a/b?old=1#fresh" Err(_) => False } - } + } expect match Url.parse("https://example.com/a/b") { @@ -1140,7 +1140,7 @@ expect Ok(resolved) => Url.to_str(resolved) == "https://example.com/root/y" Err(_) => False } - } + } expect match Url.parse("https://example.com/a?x=1#frag") { @@ -1260,7 +1260,7 @@ expect Ok(changed) => Url.to_str(changed) == "https://example.com/path#frag" Err(_) => False } - } + } expect match Url.parse("https://example.com/path") { @@ -1270,7 +1270,7 @@ expect Ok(changed) => Url.to_str(changed) == "https://example.com/path?term=caf%C3%A9&empty=" Err(_) => False } - } + } expect match Url.parse("https://example.com/path") { @@ -1286,7 +1286,7 @@ expect Ok(changed) => Url.to_str(changed) == "https://example.com/path" Err(_) => False } - } + } expect match Url.parse("https://example.com/path") { @@ -1296,7 +1296,7 @@ expect Ok(changed) => Url.to_str(changed) == "https://example.com/path#r%C3%A9sum%C3%A9/?" Err(_) => False } - } + } expect match Url.parse("https://example.com/path") { @@ -1312,7 +1312,7 @@ expect Ok(resolved) => Url.to_str(resolved) == "https://example.com/a/b?old=1" Err(_) => False } - } + } expect match Url.parse("https://example.com/a/b") { @@ -1322,7 +1322,7 @@ expect Ok(resolved) => Url.to_str(resolved) == "https://example.com/root" Err(_) => False } - } + } expect match Url.parse("https://example.com/a/b") { @@ -1332,7 +1332,7 @@ expect Ok(resolved) => Url.to_str(resolved) == "http://other.example/x" Err(_) => False } - } + } expect match Url.parse("https://example.com/a/b") { diff --git a/platform/main.roc b/platform/main.roc index a1fdb974..7533bb06 100644 --- a/platform/main.roc +++ b/platform/main.roc @@ -6,7 +6,7 @@ platform "" ## launcher-supplied program name is available from Env.program_name!. main! : List([Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))]) => Try({}, [Exit(I32), ..]) } - exposes [Cmd, Env, File, Http, IOErr, Locale, Monotonic, OsStr, Path, Random, Sleep, Sqlite, Stdin, Stdout, Stderr, Tcp, Tty, Url, Utc] + exposes [Cmd, Env, File, Http, IOErr, Locale, Monotonic, OsStr, Path, Random, Sleep, Sqlite, Stderr, Stdin, Stdout, Tcp, Tty, Url, Utc] packages { # HTTP data types (Method, Request, Response) come from the shared # roc-lang/http package so apps and other packages using it see the same @@ -15,37 +15,76 @@ platform "" } provides { "roc_main": main_for_host! } hosted { + "hosted_child_close": Host.child_close!, + "hosted_child_close_stdin": Host.child_close_stdin!, + "hosted_child_kill": Host.child_kill!, + "hosted_child_pid": Host.child_pid!, + "hosted_child_read": Host.child_read!, + "hosted_child_try_wait": Host.child_try_wait!, + "hosted_child_wait": Host.child_wait!, + "hosted_child_write": Host.child_write!, "hosted_cmd_host_exec_exit_code": Host.cmd_exec_exit_code!, "hosted_cmd_host_exec_output": Host.cmd_exec_output!, + "hosted_cmd_run": Host.cmd_run!, + "hosted_cmd_spawn": Host.cmd_spawn!, "hosted_dir_create": Host.dir_create!, "hosted_dir_create_all": Host.dir_create_all!, "hosted_dir_delete_all": Host.dir_delete_all!, "hosted_dir_delete_empty": Host.dir_delete_empty!, "hosted_dir_list": Host.dir_list!, + "hosted_env_create_temp_dir": Host.env_create_temp_dir!, "hosted_env_cwd": Host.env_cwd!, + "hosted_env_dict": Host.env_dict!, "hosted_env_exe_path": Host.env_exe_path!, + # Environment additions are appended to preserve existing hosted ABI numbering. + "hosted_env_platform": Host.env_platform!, + "hosted_env_program_name": Host.env_program_name!, + "hosted_env_set_cwd": Host.env_set_cwd!, "hosted_env_temp_dir": Host.env_temp_dir!, "hosted_env_var": Host.env_var!, "hosted_file_delete": Host.file_delete!, + # New file hosted functions are kept at the end so adding them does not + # renumber the generated glue types for existing modules. + "hosted_file_hard_link": Host.file_hard_link!, "hosted_file_is_executable": Host.file_is_executable!, "hosted_file_is_readable": Host.file_is_readable!, "hosted_file_is_writable": Host.file_is_writable!, - "hosted_file_read_bytes": Host.file_read_bytes!, - "hosted_file_read_utf8": Host.file_read_utf8!, "hosted_file_open_reader": Host.file_open_reader!, + "hosted_file_read_bytes": Host.file_read_bytes!, + "hosted_file_read_exactly": Host.file_read_exactly!, "hosted_file_read_line": Host.file_read_line!, + "hosted_file_read_up_to": Host.file_read_up_to!, + "hosted_file_read_utf8": Host.file_read_utf8!, + "hosted_file_reader_position": Host.file_reader_position!, + "hosted_file_reader_seek": Host.file_reader_seek!, + "hosted_file_rename": Host.file_rename!, "hosted_file_size_in_bytes": Host.file_size_in_bytes!, "hosted_file_time_accessed": Host.file_time_accessed!, "hosted_file_time_created": Host.file_time_created!, "hosted_file_time_modified": Host.file_time_modified!, "hosted_file_write_bytes": Host.file_write_bytes!, "hosted_file_write_utf8": Host.file_write_utf8!, + # HTTP is likewise kept at the end to avoid renumbering glue types. + "hosted_http_send_request": Host.http_send_request!, "hosted_locale_all": Host.locale_all!, "hosted_locale_get": Host.locale_get!, + "hosted_monotonic_now": Host.monotonic_now!, + "hosted_path_absolute": Host.path_absolute!, + "hosted_path_canonicalize": Host.path_canonicalize!, + "hosted_path_copy": Host.path_copy!, + "hosted_path_copy_dir": Host.path_copy_dir!, "hosted_path_type": Host.path_type!, "hosted_random_seed_u32": Host.random_seed_u32!, "hosted_random_seed_u64": Host.random_seed_u64!, "hosted_sleep_millis": Host.sleep_millis!, + "hosted_sqlite_bind": Host.sqlite_bind!, + "hosted_sqlite_column_value": Host.sqlite_column_value!, + "hosted_sqlite_columns": Host.sqlite_columns!, + # SQLite hosted functions are kept at the end so adding them does not + # renumber the generated glue types for the modules declared above. + "hosted_sqlite_prepare": Host.sqlite_prepare!, + "hosted_sqlite_reset": Host.sqlite_reset!, + "hosted_sqlite_step": Host.sqlite_step!, "hosted_stderr_line": Host.stderr_line!, "hosted_stderr_write": Host.stderr_write!, "hosted_stderr_write_bytes": Host.stderr_write_bytes!, @@ -55,58 +94,19 @@ platform "" "hosted_stdout_line": Host.stdout_line!, "hosted_stdout_write": Host.stdout_write!, "hosted_stdout_write_bytes": Host.stdout_write_bytes!, - "hosted_tty_disable_raw_mode": Host.tty_disable_raw_mode!, - "hosted_tty_enable_raw_mode": Host.tty_enable_raw_mode!, - "hosted_utc_now": Host.utc_now!, - # New file hosted functions are kept at the end so adding them does not - # renumber the generated glue types for existing modules. - "hosted_file_hard_link": Host.file_hard_link!, - "hosted_file_rename": Host.file_rename!, - # SQLite hosted functions are kept at the end so adding them does not - # renumber the generated glue types for the modules declared above. - "hosted_sqlite_prepare": Host.sqlite_prepare!, - "hosted_sqlite_bind": Host.sqlite_bind!, - "hosted_sqlite_columns": Host.sqlite_columns!, - "hosted_sqlite_column_value": Host.sqlite_column_value!, - "hosted_sqlite_step": Host.sqlite_step!, - "hosted_sqlite_reset": Host.sqlite_reset!, + "hosted_tcp_accept": Host.tcp_accept!, # TCP hosted functions are likewise kept at the end to avoid renumbering. "hosted_tcp_connect": Host.tcp_connect!, - "hosted_tcp_read_up_to": Host.tcp_read_up_to!, + "hosted_tcp_listen": Host.tcp_listen!, + "hosted_tcp_listener_close": Host.tcp_listener_close!, + "hosted_tcp_local_port": Host.tcp_local_port!, "hosted_tcp_read_exactly": Host.tcp_read_exactly!, "hosted_tcp_read_until": Host.tcp_read_until!, + "hosted_tcp_read_up_to": Host.tcp_read_up_to!, "hosted_tcp_write": Host.tcp_write!, - # HTTP is likewise kept at the end to avoid renumbering glue types. - "hosted_http_send_request": Host.http_send_request!, - # Environment additions are appended to preserve existing hosted ABI numbering. - "hosted_env_platform": Host.env_platform!, - "hosted_env_dict": Host.env_dict!, - "hosted_env_set_cwd": Host.env_set_cwd!, - "hosted_path_copy": Host.path_copy!, - "hosted_path_copy_dir": Host.path_copy_dir!, - "hosted_path_absolute": Host.path_absolute!, - "hosted_path_canonicalize": Host.path_canonicalize!, - "hosted_env_create_temp_dir": Host.env_create_temp_dir!, - "hosted_monotonic_now": Host.monotonic_now!, - "hosted_cmd_spawn": Host.cmd_spawn!, - "hosted_cmd_run": Host.cmd_run!, - "hosted_child_pid": Host.child_pid!, - "hosted_child_wait": Host.child_wait!, - "hosted_child_try_wait": Host.child_try_wait!, - "hosted_child_kill": Host.child_kill!, - "hosted_child_close": Host.child_close!, - "hosted_child_close_stdin": Host.child_close_stdin!, - "hosted_child_write": Host.child_write!, - "hosted_child_read": Host.child_read!, - "hosted_tcp_listen": Host.tcp_listen!, - "hosted_tcp_local_port": Host.tcp_local_port!, - "hosted_tcp_accept": Host.tcp_accept!, - "hosted_tcp_listener_close": Host.tcp_listener_close!, - "hosted_env_program_name": Host.env_program_name!, - "hosted_file_read_up_to": Host.file_read_up_to!, - "hosted_file_read_exactly": Host.file_read_exactly!, - "hosted_file_reader_position": Host.file_reader_position!, - "hosted_file_reader_seek": Host.file_reader_seek!, + "hosted_tty_disable_raw_mode": Host.tty_disable_raw_mode!, + "hosted_tty_enable_raw_mode": Host.tty_enable_raw_mode!, + "hosted_utc_now": Host.utc_now!, } targets: { inputs_dir: "targets/", diff --git a/scripts/bundle.py b/scripts/bundle.py index 8b51ebc8..f53d5e18 100755 --- a/scripts/bundle.py +++ b/scripts/bundle.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +import re import shutil import subprocess from pathlib import Path @@ -17,18 +18,63 @@ def relative_platform_path(path: Path) -> str: return path.relative_to(PLATFORM_DIR).as_posix() +def declared_target_inputs() -> list[Path]: + """Return every target input file that platform/main.roc declares.""" + header = (PLATFORM_DIR / "main.roc").read_text() + inputs_dir = re.search(r'inputs_dir:\s*"([^"]+)"', header).group(1) + inputs = [] + for target, files in re.findall(r"^\s+(\w+):\s*\{\s*inputs:\s*\[([^\]]*)\]", header, re.MULTILINE): + for name in re.findall(r'"([^"]+)"', files): + inputs.append(PLATFORM_DIR / inputs_dir / target / name) + return inputs + + +def create_placeholders() -> list[Path]: + """Create empty files for target inputs that were not built. + + `roc bundle` requires every declared target input to exist, but test + bundles only contain the natively built target. + """ + created = [] + for path in declared_target_inputs(): + if path.exists(): + continue + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + created.append(path) + return created + + def main() -> None: parser = argparse.ArgumentParser(description="Bundle the basic-cli platform") parser.add_argument("--output-dir", type=Path, default=ROOT) + parser.add_argument( + "--stub-missing-targets", + action="store_true", + help="create empty placeholders for unbuilt targets (test bundles only)", + ) args, roc_args = parser.parse_known_args() - output_dir = args.output_dir + placeholders = create_placeholders() if args.stub_missing_targets else [] + try: + bundle_platform(args.output_dir, roc_args) + finally: + for path in placeholders: + path.unlink(missing_ok=True) + try: + path.parent.rmdir() + except OSError: + pass + + +def bundle_platform(output_dir: Path, roc_args: list[str]) -> None: if not output_dir.is_absolute(): output_dir = ROOT / output_dir output_dir.mkdir(parents=True, exist_ok=True) output_dir = output_dir.resolve() - roc_files = sorted(PLATFORM_DIR.glob("*.roc")) + # `roc bundle` treats the first .roc file as the entry point. + roc_files = sorted(PLATFORM_DIR.glob("*.roc"), key=lambda path: (path.name != "main.roc", path.name)) library_files = sorted( path for path in (PLATFORM_DIR / "targets").rglob("*") diff --git a/scripts/test.py b/scripts/test.py index 432aefbb..085f7c9d 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -140,7 +140,7 @@ def stage_enabled(defaults: dict[str, bool], app: dict[str, object], stage: str) def create_bundle() -> Path: result = subprocess.run( - [sys.executable, str(ROOT / "scripts" / "bundle.py")], + [sys.executable, str(ROOT / "scripts" / "bundle.py"), "--stub-missing-targets"], cwd=ROOT, text=True, stdout=subprocess.PIPE,